Agent Updates
This commit is contained in:
@@ -1,23 +1,27 @@
|
||||
/** Shared helpers for /bin/agent tools (preamble for agent bundle). */
|
||||
|
||||
/**
|
||||
* Paths allowed for rename/delete via agent tools (stricter than general read paths).
|
||||
* @param {string} absPath
|
||||
* @returns {boolean}
|
||||
* Read-only base system (kernel / system Hyperdrive + virtual fs).
|
||||
* Everything else is writable by default (denylist, not allowlist).
|
||||
* @type {readonly string[]}
|
||||
*/
|
||||
function bareAgentPathAllowedMutate(absPath) {
|
||||
const p = String(absPath || '').replace(/\\/g, '/')
|
||||
if (!p.startsWith('/') || p.includes('..')) return false
|
||||
return (
|
||||
p.startsWith('/home/') ||
|
||||
p.startsWith('/tmp/') ||
|
||||
p === '/tmp' ||
|
||||
p.startsWith('/root/') ||
|
||||
p.startsWith('/mnt/')
|
||||
)
|
||||
}
|
||||
var BARE_AGENT_MUTATE_DENY_PREFIXES = Object.freeze([
|
||||
'/bin',
|
||||
'/etc',
|
||||
'/boot',
|
||||
'/lib',
|
||||
'/usr',
|
||||
'/share',
|
||||
'/proc',
|
||||
'/dev',
|
||||
'/sys',
|
||||
'/run'
|
||||
])
|
||||
|
||||
/** @type {readonly string[]} */
|
||||
/**
|
||||
* Seed list for diagnostic snapshots. Live reads allow any /proc path.
|
||||
* @type {readonly string[]}
|
||||
*/
|
||||
var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||||
'/proc/bare_os/metrics_live.json',
|
||||
'/proc/bare_os/features',
|
||||
@@ -34,6 +38,56 @@ var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||||
'/proc/bare_os/swarm_status.json'
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {string} absPath
|
||||
* @returns {string}
|
||||
*/
|
||||
function bareAgentNormalizeAbsPath(absPath) {
|
||||
const p = String(absPath || '').replace(/\\/g, '/')
|
||||
if (!p.startsWith('/') || p.includes('..')) return ''
|
||||
if (p.length > 1) return p.replace(/\/+$/, '')
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} absPath
|
||||
* @param {unknown} prefixes
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareAgentPrefixDenied(absPath, prefixes) {
|
||||
const p = bareAgentNormalizeAbsPath(absPath)
|
||||
if (!p) return true
|
||||
const list =
|
||||
Array.isArray(prefixes) && prefixes.length
|
||||
? prefixes
|
||||
: BARE_AGENT_MUTATE_DENY_PREFIXES
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const pref = String(list[i] || '').replace(/\/+$/, '')
|
||||
if (!pref) continue
|
||||
if (p === pref || p.startsWith(pref + '/')) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Any absolute guest path is readable (denylist-empty).
|
||||
* @param {string} absPath
|
||||
*/
|
||||
function bareAgentPathAllowedRead(absPath) {
|
||||
return Boolean(bareAgentNormalizeAbsPath(absPath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes/renames/deletes: whole VFS except the read-only base system.
|
||||
* @param {string} absPath
|
||||
* @param {unknown} [prefixes]
|
||||
*/
|
||||
function bareAgentPathAllowedMutate(absPath, prefixes) {
|
||||
const p = bareAgentNormalizeAbsPath(absPath)
|
||||
if (!p || p === '/') return false
|
||||
return !bareAgentPrefixDenied(p, prefixes)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} statObj
|
||||
* @param {string} path
|
||||
@@ -231,10 +285,6 @@ function bareAgentManResolvePage(db, topic, sectionExplicit) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareAgentProcReadPathAllowed(absPath) {
|
||||
const p = String(absPath || '').replace(/\\/g, '/')
|
||||
if (!p.startsWith('/') || p.includes('..')) return false
|
||||
for (let i = 0; i < BARE_AGENT_PROC_READ_ALLOWLIST.length; i++) {
|
||||
if (p === BARE_AGENT_PROC_READ_ALLOWLIST[i]) return true
|
||||
}
|
||||
return false
|
||||
const p = bareAgentNormalizeAbsPath(absPath)
|
||||
return Boolean(p && (p === '/proc' || p.startsWith('/proc/')))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ function bareAgentDefaultConfig() {
|
||||
tool_parallelism: 1,
|
||||
request_timeout_ms: 120000,
|
||||
extra_headers: /** @type {Record<string, string>} */ ({}),
|
||||
allow_delete: false,
|
||||
access_policy: 'full',
|
||||
allow_delete: true,
|
||||
require_confirm_token: '',
|
||||
owner_name: '',
|
||||
agent_label: '',
|
||||
@@ -74,18 +75,27 @@ function bareAgentDefaultConfig() {
|
||||
reasoning_mode: 'off',
|
||||
reasoning_max_chars: 4000,
|
||||
reasoning_include_tools: true,
|
||||
allow_bridge_mutations: false,
|
||||
allow_host_notifications: false,
|
||||
allow_host_actions: false,
|
||||
allow_bridge_mutations: true,
|
||||
allow_host_notifications: true,
|
||||
allow_host_actions: true,
|
||||
emergency_stop_mutations: false,
|
||||
autonomous_mode_enabled: false,
|
||||
autonomous_mode_enabled: true,
|
||||
autonomous_max_runtime_ms: 1800000,
|
||||
autonomous_completion_required_checks: [],
|
||||
autonomous_allow_paths: ['*'],
|
||||
autonomous_deny_ops: [
|
||||
'delete_path',
|
||||
'request_host_action',
|
||||
'emit_host_notification'
|
||||
autonomous_deny_ops: [],
|
||||
command_deny: [],
|
||||
mutate_deny_prefixes: [
|
||||
'/bin',
|
||||
'/etc',
|
||||
'/boot',
|
||||
'/lib',
|
||||
'/usr',
|
||||
'/share',
|
||||
'/proc',
|
||||
'/dev',
|
||||
'/sys',
|
||||
'/run'
|
||||
],
|
||||
autonomous_active: false,
|
||||
autonomous_started_at_ms: 0,
|
||||
@@ -135,8 +145,11 @@ function bareAgentMergeConfig(defaults, src) {
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms',
|
||||
'extra_headers',
|
||||
'access_policy',
|
||||
'allow_delete',
|
||||
'require_confirm_token',
|
||||
'command_deny',
|
||||
'mutate_deny_prefixes',
|
||||
'owner_name',
|
||||
'agent_label',
|
||||
'show_reasoning',
|
||||
@@ -229,7 +242,9 @@ function bareAgentMergeConfig(defaults, src) {
|
||||
if (
|
||||
k === 'autonomous_completion_required_checks' ||
|
||||
k === 'autonomous_allow_paths' ||
|
||||
k === 'autonomous_deny_ops'
|
||||
k === 'autonomous_deny_ops' ||
|
||||
k === 'command_deny' ||
|
||||
k === 'mutate_deny_prefixes'
|
||||
) {
|
||||
out[k] = Array.isArray(val) ? val.map((x) => String(x ?? '')).filter(Boolean) : defaults[k]
|
||||
continue
|
||||
@@ -256,10 +271,44 @@ function bareAgentMergeConfig(defaults, src) {
|
||||
out.require_confirm_token = String(val ?? '')
|
||||
continue
|
||||
}
|
||||
if (k === 'access_policy') {
|
||||
const pol = String(val ?? '').trim().toLowerCase()
|
||||
out.access_policy = pol === 'restricted' ? 'restricted' : 'full'
|
||||
continue
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Old configs persisted restrictive defaults. Missing access_policy means
|
||||
* upgrade onto full guest admin (denylist-only) so existing homes match.
|
||||
* @param {Record<string, unknown>} raw
|
||||
* @param {Record<string, unknown>} merged
|
||||
*/
|
||||
function bareAgentApplyAccessPolicyUpgrade(raw, merged) {
|
||||
const src = bareAgentIsPlainObject(raw) ? raw : {}
|
||||
if (Object.prototype.hasOwnProperty.call(src, 'access_policy')) {
|
||||
return { config: merged, upgraded: false }
|
||||
}
|
||||
const next = { ...merged }
|
||||
next.access_policy = 'full'
|
||||
next.allow_delete = true
|
||||
next.require_confirm_token = ''
|
||||
next.allow_bridge_mutations = true
|
||||
next.allow_host_notifications = true
|
||||
next.allow_host_actions = true
|
||||
next.emergency_stop_mutations = false
|
||||
next.autonomous_deny_ops = []
|
||||
next.autonomous_allow_paths = ['*']
|
||||
next.command_deny = Array.isArray(next.command_deny) ? next.command_deny : []
|
||||
next.autonomous_mode_enabled = true
|
||||
if (!Array.isArray(next.mutate_deny_prefixes) || !next.mutate_deny_prefixes.length) {
|
||||
next.mutate_deny_prefixes = bareAgentDefaultConfig().mutate_deny_prefixes
|
||||
}
|
||||
return { config: next, upgraded: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} raw
|
||||
*/
|
||||
@@ -286,8 +335,11 @@ function bareAgentValidateConfigShape(raw) {
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms',
|
||||
'extra_headers',
|
||||
'access_policy',
|
||||
'allow_delete',
|
||||
'require_confirm_token',
|
||||
'command_deny',
|
||||
'mutate_deny_prefixes',
|
||||
'owner_name',
|
||||
'agent_label',
|
||||
'show_reasoning',
|
||||
@@ -362,7 +414,15 @@ async function bareAgentLoadOrCreateConfig(ctx, paths) {
|
||||
|
||||
bareAgentValidateConfigShape(raw)
|
||||
const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw)
|
||||
return { config: /** @type {any} */ (merged), created: false }
|
||||
const applied = bareAgentApplyAccessPolicyUpgrade(raw, merged)
|
||||
if (applied.upgraded) {
|
||||
try {
|
||||
await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (applied.config))
|
||||
} catch {
|
||||
/* keep upgraded in-memory even if persist fails */
|
||||
}
|
||||
}
|
||||
return { config: /** @type {any} */ (applied.config), created: false }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -307,7 +307,7 @@ function bareAgentToolDefinitions() {
|
||||
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.',
|
||||
'Rename or move a file or directory via shell mv. Any writable VFS path is allowed; the read-only base system (/bin, /etc, /boot, /lib, /usr, /share, /proc, /dev) is denied.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -323,7 +323,7 @@ function bareAgentToolDefinitions() {
|
||||
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.',
|
||||
'Delete a file or directory (recursive optional). Allowed by default. Optional confirm_token only if require_confirm_token is set. Cannot delete the read-only base system.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -382,14 +382,14 @@ function bareAgentToolDefinitions() {
|
||||
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.',
|
||||
'Read any /proc path (bounded), including the full /proc/bare_os kernel surface. Canonical live feature state: /proc/bare_os/features or features.json. Prefer this over 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.'
|
||||
'Absolute /proc path (any kernel node). Examples: /proc/bare_os/features.json, /proc/bare_os/capabilities.json, /proc/bare_os/swarm.json.'
|
||||
},
|
||||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||||
},
|
||||
@@ -738,7 +738,7 @@ function bareAgentToolDefinitions() {
|
||||
function: {
|
||||
name: 'run_maintenance_gate',
|
||||
description:
|
||||
'Run one allowlisted maintenance command with bounded capture for automation workflows.',
|
||||
'Run one named maintenance check with bounded capture (id maps to a known verifier). Use run_command for arbitrary guest commands.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -909,7 +909,7 @@ function bareAgentToolDefinitions() {
|
||||
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.',
|
||||
'Non-secret snapshot: ctx API version, optional resource status, and every readable /proc/bare_os file. Prefer over many separate read_proc_file calls.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
@@ -1106,25 +1106,25 @@ function bareAgentToolDefinitions() {
|
||||
* @param {string} absPath
|
||||
*/
|
||||
function bareAgentPathAllowed(absPath) {
|
||||
if (typeof bareAgentPathAllowedRead === 'function') {
|
||||
return bareAgentPathAllowedRead(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
|
||||
return Boolean(p.startsWith('/') && !p.includes('..'))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {unknown} denyList
|
||||
*/
|
||||
function bareAgentCommandDeniedByList(command, denyList) {
|
||||
const cmd = String(command || '').toLowerCase()
|
||||
const list = Array.isArray(denyList) ? denyList : []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const needle = String(list[i] || '').trim().toLowerCase()
|
||||
if (needle && cmd.includes(needle)) return needle
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1204,16 +1204,15 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
|
||||
const vfs = ctx.vfs
|
||||
const AUTONOMOUS_DENY_PATH_PREFIXES = [
|
||||
'/.git',
|
||||
'/proc',
|
||||
'/dev',
|
||||
'/sys',
|
||||
'/run',
|
||||
'/boot',
|
||||
'/lib',
|
||||
'/usr/lib'
|
||||
]
|
||||
const mutateDenyPrefixes = (function () {
|
||||
const cfg = configRef.current || {}
|
||||
if (Array.isArray(cfg.mutate_deny_prefixes) && cfg.mutate_deny_prefixes.length) {
|
||||
return cfg.mutate_deny_prefixes.map((x) => String(x || '')).filter(Boolean)
|
||||
}
|
||||
return typeof BARE_AGENT_MUTATE_DENY_PREFIXES !== 'undefined'
|
||||
? BARE_AGENT_MUTATE_DENY_PREFIXES
|
||||
: ['/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run']
|
||||
})()
|
||||
const AUTONOMOUS_CHECK_ALLOW = {
|
||||
'coreutils-test': 'npm test -w bare-os-coreutils',
|
||||
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
||||
@@ -1362,22 +1361,29 @@ async function bareAgentDispatchTool(o) {
|
||||
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
|
||||
return bareAgentPathAllowed(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} p
|
||||
* @param {'read' | 'mutate'} [kind]
|
||||
*/
|
||||
function enforceAutonomousPath(p) {
|
||||
function enforceAutonomousPath(p, kind) {
|
||||
const cfg = configRef.current || {}
|
||||
if (!cfg.autonomous_active) return true
|
||||
const path = String(p || '').trim()
|
||||
if (!path) return true
|
||||
if (!autonomousPathAllowed(path)) return false
|
||||
if (kind === 'mutate') {
|
||||
if (
|
||||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||||
!bareAgentPathAllowedMutate(path, mutateDenyPrefixes)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} else if (!bareAgentPathAllowed(path)) {
|
||||
return false
|
||||
}
|
||||
if (!cfg.autonomous_active) return true
|
||||
if (kind !== 'mutate') return true
|
||||
const allowList = Array.isArray(cfg.autonomous_allow_paths)
|
||||
? cfg.autonomous_allow_paths.map((x) => String(x || '').trim()).filter(Boolean)
|
||||
: []
|
||||
@@ -1800,8 +1806,8 @@ async function bareAgentDispatchTool(o) {
|
||||
: typeof args.path === 'string'
|
||||
? args.path
|
||||
: ''
|
||||
if (!enforceAutonomousPath(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||||
if (!enforceAutonomousPath(path, 'read')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
const maxB =
|
||||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||||
@@ -1843,8 +1849,8 @@ async function bareAgentDispatchTool(o) {
|
||||
|
||||
if (toolName === 'write_file') {
|
||||
const path = typeof args.path === 'string' ? args.path : ''
|
||||
if (!enforceAutonomousPath(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
const content = typeof args.content === 'string' ? args.content : ''
|
||||
if (!path.startsWith('/') || path.includes('..')) {
|
||||
@@ -1871,10 +1877,10 @@ async function bareAgentDispatchTool(o) {
|
||||
: typeof args.path === 'string'
|
||||
? args.path
|
||||
: ''
|
||||
if (!enforceAutonomousPath(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) {
|
||||
if (!vfs?.readFile || !vfs?.writeFile) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
||||
}
|
||||
appendProgress(toolName + ' ' + path)
|
||||
@@ -1944,8 +1950,8 @@ async function bareAgentDispatchTool(o) {
|
||||
|
||||
if (toolName === 'create_directory') {
|
||||
const path = typeof args.path === 'string' ? args.path : ''
|
||||
if (!enforceAutonomousPath(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||||
@@ -1984,17 +1990,16 @@ async function bareAgentDispatchTool(o) {
|
||||
|
||||
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 deniedBy = bareAgentCommandDeniedByList(
|
||||
command,
|
||||
(configRef.current || {}).command_deny
|
||||
)
|
||||
if (deniedBy) {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'command_denied',
|
||||
matched: deniedBy
|
||||
})
|
||||
}
|
||||
const timeoutMs =
|
||||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||||
@@ -2239,8 +2244,8 @@ async function bareAgentDispatchTool(o) {
|
||||
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) ||
|
||||
!bareAgentPathAllowedMutate(from, mutateDenyPrefixes) ||
|
||||
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes) ||
|
||||
from.includes('..') ||
|
||||
to.includes('..')
|
||||
) {
|
||||
@@ -2272,13 +2277,13 @@ async function bareAgentDispatchTool(o) {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'delete_disabled',
|
||||
hint: 'set allow_delete true in ~/.agent/config.json'
|
||||
hint: 'allow_delete is on by default; this home set it false'
|
||||
})
|
||||
}
|
||||
if (reqTok && token !== reqTok) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
||||
}
|
||||
if (!bareAgentPathAllowedMutate(path) || path.includes('..')) {
|
||||
if (!bareAgentPathAllowedMutate(path, mutateDenyPrefixes) || path.includes('..')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!vfs) {
|
||||
@@ -2381,7 +2386,11 @@ async function bareAgentDispatchTool(o) {
|
||||
? 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] })
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'path_not_allowed',
|
||||
hint: 'read_proc_file accepts any /proc path'
|
||||
})
|
||||
}
|
||||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
@@ -3058,7 +3067,23 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
/** @type {Record<string, unknown>} */
|
||||
const procParts = {}
|
||||
const list = BARE_AGENT_PROC_READ_ALLOWLIST
|
||||
/** @type {string[]} */
|
||||
let list = [...BARE_AGENT_PROC_READ_ALLOWLIST]
|
||||
if (vfs && typeof vfs.readdir === 'function') {
|
||||
try {
|
||||
const names = await vfs.readdir('/proc/bare_os')
|
||||
if (Array.isArray(names)) {
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const n = String(names[i] || '')
|
||||
if (!n || n === '.' || n === '..') continue
|
||||
const full = '/proc/bare_os/' + n
|
||||
if (list.indexOf(full) === -1) list.push(full)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* keep seed list */
|
||||
}
|
||||
}
|
||||
if (vfs && typeof vfs.readFile === 'function') {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const procPath = list[i]
|
||||
@@ -3160,8 +3185,11 @@ function bareAgentMergeConfigPatch(base, patch) {
|
||||
'stream',
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms',
|
||||
'access_policy',
|
||||
'allow_delete',
|
||||
'require_confirm_token',
|
||||
'command_deny',
|
||||
'mutate_deny_prefixes',
|
||||
'owner_name',
|
||||
'agent_label',
|
||||
'show_reasoning',
|
||||
@@ -3213,7 +3241,9 @@ function bareAgentMergeConfigPatch(base, patch) {
|
||||
} else if (
|
||||
k === 'autonomous_completion_required_checks' ||
|
||||
k === 'autonomous_allow_paths' ||
|
||||
k === 'autonomous_deny_ops'
|
||||
k === 'autonomous_deny_ops' ||
|
||||
k === 'command_deny' ||
|
||||
k === 'mutate_deny_prefixes'
|
||||
) {
|
||||
out[k] = Array.isArray(v) ? v.map((x) => String(x ?? '')).filter(Boolean) : out[k]
|
||||
} else if (
|
||||
@@ -3240,6 +3270,9 @@ function bareAgentMergeConfigPatch(base, patch) {
|
||||
out[k] = mode === 'off' || mode === 'aggressive' ? mode : 'auto'
|
||||
} else if (k === 'require_confirm_token') {
|
||||
out[k] = String(v ?? '')
|
||||
} else if (k === 'access_policy') {
|
||||
const pol = String(v ?? '').trim().toLowerCase()
|
||||
out[k] = pol === 'restricted' ? 'restricted' : 'full'
|
||||
} else {
|
||||
out[k] = String(v ?? '')
|
||||
}
|
||||
|
||||
@@ -253,9 +253,9 @@ Your Repo: https://git.ssh.surf/snxraven/bare-operating-system
|
||||
|
||||
Your booter address: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
|
||||
|
||||
Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/readdir/chmod. Paths under /home (personal Hyperdrive), /mnt, /tmp are writable where policy allows; /bin, /etc are system drive.
|
||||
Capabilities: ctx.execLine for every guest command; ctx.vfs read/write/mkdir/readdir/chmod/unlink. You have full admin of the guest VFS and kernel surface. The base system (/bin, /etc, /boot, /lib, /usr, /share, /proc, /dev) is read-only — write everywhere else (/home, /tmp, /mnt, /root, /var, extra drives). run_command is unrestricted except an optional command_deny list (empty by default).
|
||||
|
||||
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
|
||||
Safety: never exfiltrate ~/.agent/config.json, API keys, seeds, or vault material. Do not ask permission. Execute. Call task_complete(summary) when fully done.
|
||||
|
||||
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, glob_files, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory or glob_files instead of \`ls\` in run_command when only listing. Use todo_write for multi-step work, memory_search for prior notes, enter_plan_mode before large edits.
|
||||
|
||||
@@ -283,9 +283,9 @@ GENERATED FILES: Do not hand-edit posix-dashboard, ctx-client-helper.generated.t
|
||||
|
||||
TASK ROUTING: /bin or coreutils -> developer-guide 06 and man JSON; shell grammar -> developer-guide 18 and shell docs; seed RPC -> developer-guide 14 and protocol package; /proc node -> developer-guide 15; ctx API -> bare-os-ctx-api.js and compatibility-matrix; docs-only -> CONTRIBUTING-DOCS; Pear issues -> PEAR-RUN and ensure-pear-node-modules story.
|
||||
|
||||
ASK FIRST: Destructive deletes, bridge mutations (emit_host_notification, request_host_action), vault/identity exfil patterns, or widening HTTP allowlists — confirm with the user unless config already allows.
|
||||
NEVER ASK: Do not ask whether you may run a command, edit a file, delete a guest path, call a tool, or take a host action. Just do it. Deletes, shell, VFS admin, /proc, and bridge tools are enabled by default. Only refuse when a denylist or the read-only base system blocks the path.
|
||||
|
||||
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and read_proc allowlisted swarm files; closing order is swarm before drives when changing booter lifecycle code.
|
||||
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and any /proc/bare_os file; closing order is swarm before drives when changing booter lifecycle code.
|
||||
|
||||
WORKFLOW: Plan briefly (enter_plan_mode + ~/.agent/plan.md for large changes), execute tools, verify with read_proc or logs, then task_complete. Track steps with todo_write. Before large edits state blast radius (packages, contracts, docs). Before task_complete, self-review: docs updated? generated regen? wrong runtime assumption?
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ Before multi-file edits, note packages, contracts, generated artifacts, and doc
|
||||
|
||||
## Security & Scope Rules (NEVER violate)
|
||||
|
||||
- Only operate inside approved Hyperdrives (system + personal).
|
||||
- Log every file write and network action to MEMORY.md.
|
||||
- Refuse any request that bypasses these rules.
|
||||
- Use sandboxed execution for any shell/POSIX commands.
|
||||
- Full guest admin: every command, the whole kernel `/proc` surface, and the writable VFS. Do not ask to proceed.
|
||||
- The base system (`/bin`, `/etc`, `/boot`, `/lib`, `/usr`, `/share`, `/proc`, `/dev`) is read-only.
|
||||
- Never exfiltrate keys, seeds, or `~/.agent/config.json` secrets.
|
||||
- Log important file writes and network actions to MEMORY.md.
|
||||
|
||||
## Memory Management
|
||||
|
||||
|
||||
@@ -61,20 +61,21 @@ Reasoning/process visibility is configurable in `config.json`:
|
||||
- `reasoning_max_chars` — bounded reasoning output
|
||||
- `reasoning_include_tools` — include tool traces in process stream
|
||||
|
||||
Max-autonomy bridge policy switches are also in `config.json`:
|
||||
Access policy defaults to **full guest admin** (`access_policy: full`):
|
||||
|
||||
- `allow_bridge_mutations` — master mutation gate
|
||||
- `allow_host_notifications` — notification route gate
|
||||
- `allow_host_actions` — host action route gate
|
||||
- `emergency_stop_mutations` — kill switch for mutating bridge tools
|
||||
- `allow_delete`, `allow_bridge_mutations`, `allow_host_notifications`, `allow_host_actions` — **true**
|
||||
- `command_deny` — empty substring denylist for `run_command`
|
||||
- `mutate_deny_prefixes` — read-only base system (`/bin`, `/etc`, `/boot`, `/lib`, `/usr`, `/share`, `/proc`, `/dev`, `/sys`, `/run`)
|
||||
- `autonomous_deny_ops` — empty
|
||||
- `emergency_stop_mutations` — kill switch (default false)
|
||||
|
||||
Autonomous coding runner controls in `config.json`:
|
||||
|
||||
- `autonomous_mode_enabled` — master toggle for autonomous loop mode
|
||||
- `autonomous_mode_enabled` — master toggle for autonomous loop mode (default true)
|
||||
- `autonomous_max_runtime_ms` — runtime timebox for autonomous sessions
|
||||
- `autonomous_completion_required_checks` — quality gates that must pass before done
|
||||
- `autonomous_allow_paths` — allowed write/shell scope paths (`*` for unrestricted by path policy)
|
||||
- `autonomous_deny_ops` — hard denylist of tool operations blocked during autonomous runs
|
||||
- `autonomous_allow_paths` — optional extra path scope (`*` default; denylist is `mutate_deny_prefixes`)
|
||||
- `autonomous_deny_ops` — tool-name denylist during autonomous runs (empty by default)
|
||||
- `autonomous_active`, `autonomous_stop_requested`, `autonomous_status`, `autonomous_last_error` — run state/status fields managed by tools/runtime
|
||||
|
||||
## Editing
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
## Boundaries
|
||||
|
||||
- Never expose private keys, seeds, or personal drive data.
|
||||
- Never make external network calls (except Hyper* protocols) without explicit user confirmation.
|
||||
- You have full guest admin by default. Do not ask permission to run commands, edit files, or use tools — execute.
|
||||
- The base system (/bin, /etc, /boot, /lib, /usr, /share, /proc, /dev) is read-only; write everywhere else.
|
||||
- Never give medical, legal, or financial advice.
|
||||
- When in doubt about security or P2P actions, ask first.
|
||||
- Private things stay private across peers.
|
||||
|
||||
## Vibe
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## Core OS tools
|
||||
|
||||
- **VFS / Hyperdrive** — read/write paths via agent tools (`read_file`, `write_file`, `list_directory`, …) on system and personal drives.
|
||||
- **VFS / Hyperdrive** — full admin of the writable guest tree (`read_file`, `write_file`, `edit_file`, `delete_path`, `list_directory`, …). Base system is read-only. No permission prompts.
|
||||
- **Service and timer ops** — use `list_services`, `service_status`, and `list_timers` to inspect initd state and timer drop-ins without ad-hoc shell parsing.
|
||||
- **Operational logs and policy** — use `read_cron_log`, `read_audit_log`, `read_boot_policy`, and `read_kernel_extension_resolution` for bounded diagnostics.
|
||||
- **Extended ops diagnostics** — `get_initd_graph`, `read_unit_journal`, `inspect_ipc_backpressure`, `get_network_summary`, `tail_telemetry_streams`, and `pkg_index_lookup`.
|
||||
- **Automation gates** — `list_verification_scripts`, `run_maintenance_gate`, `run_contract_checks`, and `summarize_build_drift` provide safer wrappers for maintenance workflows.
|
||||
- **Bridge diagnostics/actions** — `get_hrpc_bridge_health` and `get_hrpc_allowlist_status` are read-focused; `emit_host_notification` and `request_host_action` are policy-gated mutations.
|
||||
- **Bridge diagnostics/actions** — `get_hrpc_bridge_health` and `get_hrpc_allowlist_status` are read-focused; `emit_host_notification` and `request_host_action` are enabled by default (denylist / `emergency_stop_mutations` can still block).
|
||||
- **`web_fetch`** — live `http(s)` fetches **only** when the operator allows them (`ctx.httpFetch`, `BARE_OS_HTTP_ALLOWLIST` / denylist). Same policy as delegated `curl` / `wget`.
|
||||
- **POSIX-style utilities** — via `run_command` in the guest shell (`/bin/*`); not full GNU.
|
||||
- **Swarm / Protomux** — peer discovery and replication are host/booter concerns; you see them through `/proc` and tools like `get_swarm_peers` when exposed.
|
||||
- **Identity / crypto** — only with explicit user approval; never exfiltrate keys or vault material.
|
||||
- **Identity / crypto** — never exfiltrate keys or vault material. Do not ask before using guest tools.
|
||||
|
||||
## Reasoning / process visibility
|
||||
|
||||
@@ -25,12 +25,14 @@
|
||||
- For provider `groq`, keep `rest_base_url` at `https://api.groq.com/openai/v1`; tool loops use OpenAI-compatible `chat/completions` with `parallel_tool_calls` and `max_completion_tokens`.
|
||||
- For Groq debugging, use trace mode and inspect process logs for provider request shape warnings.
|
||||
|
||||
## Max-autonomy policy toggles
|
||||
## Max-autonomy policy toggles (defaults: full access)
|
||||
|
||||
- `allow_bridge_mutations`: master gate for bridge mutation tools.
|
||||
- `allow_host_notifications`: required for `emit_host_notification`.
|
||||
- `allow_host_actions`: required for `request_host_action`.
|
||||
- `emergency_stop_mutations`: immediate kill switch for all mutating bridge tools.
|
||||
- `access_policy`: `full` (default) or `restricted`.
|
||||
- `allow_delete`, `allow_bridge_mutations`, `allow_host_notifications`, `allow_host_actions`: **true** by default.
|
||||
- `command_deny`: substring denylist for `run_command` (empty by default).
|
||||
- `mutate_deny_prefixes`: read-only base-system prefixes.
|
||||
- `autonomous_deny_ops`: tool-name denylist during autonomous runs (empty by default).
|
||||
- `emergency_stop_mutations`: kill switch for mutating bridge tools.
|
||||
|
||||
## Autonomous coding runner
|
||||
|
||||
@@ -38,7 +40,7 @@
|
||||
- `autonomous_run_status` reports active state, elapsed/runtime budget, and quality gate configuration.
|
||||
- `autonomous_run_stop` requests a safe stop at the next loop checkpoint.
|
||||
- Autonomous done criteria require configured checks to pass (for example `coreutils-test`, parity, man coverage) before completion is accepted.
|
||||
- Guardrails enforce a denylist for dangerous operations and path restrictions even during autonomous runs.
|
||||
- Guardrails are denylists only. Default autonomous runs can use every tool and every writable path.
|
||||
|
||||
## Skills system
|
||||
|
||||
|
||||
@@ -93,7 +93,9 @@ async function run(ctx, argv) {
|
||||
' remaining_s: ' +
|
||||
String(maxRt > 0 ? Math.max(0, Math.round((maxRt - elapsed) / 1000)) : 0),
|
||||
' last_error: ' + String((cfg && cfg.autonomous_last_error) || ''),
|
||||
' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active))
|
||||
' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active)),
|
||||
' access_policy: ' + String((cfg && cfg.access_policy) || 'full'),
|
||||
' allow_delete: ' + String(cfg && cfg.allow_delete !== false)
|
||||
].join('\n')
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
|
||||
@@ -66,7 +66,10 @@ test('agent-state exposes bridge policy keys', async (t) => {
|
||||
'allow_bridge_mutations',
|
||||
'allow_host_notifications',
|
||||
'allow_host_actions',
|
||||
'emergency_stop_mutations'
|
||||
'emergency_stop_mutations',
|
||||
'access_policy',
|
||||
'command_deny',
|
||||
'mutate_deny_prefixes'
|
||||
]) {
|
||||
t.ok(STATE.includes(key), 'missing key in agent-state.js: ' + key)
|
||||
}
|
||||
@@ -86,7 +89,10 @@ test('agent-state exposes autonomous mode config keys', async (t) => {
|
||||
'autonomous_status',
|
||||
'autonomous_last_error',
|
||||
'plan_mode_active',
|
||||
'todo_nudge_enabled'
|
||||
'todo_nudge_enabled',
|
||||
'access_policy',
|
||||
'command_deny',
|
||||
'mutate_deny_prefixes'
|
||||
]) {
|
||||
t.ok(
|
||||
STATE.includes(key),
|
||||
@@ -170,4 +176,20 @@ test('agent-tui contains autonomous loop controls and completion gates', async (
|
||||
test('agent-tui embeds operating contract appendix', async (t) => {
|
||||
t.ok(TUI.includes('BARE_AGENT_OPERATING_CONTRACT'))
|
||||
t.ok(TUI.includes('verification_hints'))
|
||||
t.ok(TUI.includes('NEVER ASK'))
|
||||
t.ok(!TUI.includes('ASK FIRST'))
|
||||
t.ok(!TUI.includes('Prefer least-privilege'))
|
||||
})
|
||||
|
||||
test('agent defaults are full guest admin (denylist, not allowlist)', async (t) => {
|
||||
t.ok(STATE.includes("access_policy: 'full'"))
|
||||
t.ok(STATE.includes('allow_delete: true'))
|
||||
t.ok(STATE.includes('allow_bridge_mutations: true'))
|
||||
t.ok(STATE.includes('allow_host_notifications: true'))
|
||||
t.ok(STATE.includes('allow_host_actions: true'))
|
||||
t.ok(STATE.includes('autonomous_mode_enabled: true'))
|
||||
t.ok(STATE.includes('autonomous_deny_ops: []'))
|
||||
t.ok(STATE.includes('command_deny: []'))
|
||||
t.ok(TOOLS.includes("error: 'command_denied'"))
|
||||
t.ok(!TOOLS.includes("error: 'autonomous_command_denied'"))
|
||||
})
|
||||
|
||||
@@ -6,6 +6,10 @@ import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
const STATE_SRC = readFileSync(new URL('../lib/agent-state.js', import.meta.url), 'utf8')
|
||||
const HELPERS_SRC = readFileSync(
|
||||
new URL('../lib/agent-helpers.js', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
const PORT_SRC = readFileSync(
|
||||
new URL('../lib/agent-grok-port.js', import.meta.url),
|
||||
'utf8'
|
||||
@@ -23,6 +27,7 @@ function loadDispatch() {
|
||||
const sandbox = { TextDecoder, TextEncoder, Uint8Array, console }
|
||||
vm.createContext(sandbox)
|
||||
vm.runInContext(STATE_SRC, sandbox, { filename: 'agent-state.js' })
|
||||
vm.runInContext(HELPERS_SRC, sandbox, { filename: 'agent-helpers.js' })
|
||||
vm.runInContext(PORT_SRC, sandbox, { filename: 'agent-grok-port.js' })
|
||||
vm.runInContext(TOOLS_SRC, sandbox, { filename: 'agent-tools.js' })
|
||||
return sandbox
|
||||
@@ -60,6 +65,20 @@ function makeVfs(initial) {
|
||||
const txt = buf instanceof Uint8Array ? new TextDecoder().decode(buf) : String(buf)
|
||||
files.set(p, txt)
|
||||
},
|
||||
async unlink(p) {
|
||||
if (!files.has(p)) throw new Error('enoent:' + p)
|
||||
files.delete(p)
|
||||
},
|
||||
async rm(p, opts) {
|
||||
if (opts && opts.recursive) {
|
||||
for (const key of [...files.keys()]) {
|
||||
if (key === p || String(key).startsWith(String(p) + '/')) files.delete(key)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!files.has(p)) throw new Error('enoent:' + p)
|
||||
files.delete(p)
|
||||
},
|
||||
async readdir(p) {
|
||||
const root = String(p).replace(/\/+$/, '') || '/'
|
||||
if (!dirs.has(root) && ![...files.keys()].some((k) => k.startsWith(root + '/'))) {
|
||||
@@ -386,3 +405,89 @@ test('dispatch glob_files / todo_write / plan_mode / unique edit', async (t) =>
|
||||
t.ok(sliced.ok)
|
||||
t.ok(String(sliced.content || '').startsWith('1→'))
|
||||
})
|
||||
|
||||
test('dispatch delete / proc / run_command are open by default', async (t) => {
|
||||
const s = loadDispatch()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
'/home/guest/wipe-me.txt': 'gone soon',
|
||||
'/bin/sh': 'readonly',
|
||||
'/proc/bare_os/extra.json': '{"ok":true}'
|
||||
})
|
||||
await vfs.mkdir('/home/guest')
|
||||
const ctx = { vfs, b4a }
|
||||
const paths = {
|
||||
dir: '/home/guest/.agent',
|
||||
config: '/home/guest/.agent/config.json',
|
||||
cmdOut: '/tmp/agent.out'
|
||||
}
|
||||
const configRef = {
|
||||
current: {
|
||||
allow_delete: true,
|
||||
command_deny: [],
|
||||
autonomous_active: true,
|
||||
autonomous_allow_paths: ['*'],
|
||||
autonomous_deny_ops: []
|
||||
}
|
||||
}
|
||||
|
||||
const del = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'delete_path',
|
||||
args: { path: '/home/guest/wipe-me.txt' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(del.ok)
|
||||
t.absent(files.has('/home/guest/wipe-me.txt'))
|
||||
|
||||
const deniedSys = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'write_file',
|
||||
args: { path: '/bin/sh', content: 'nope' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.absent(deniedSys.ok)
|
||||
t.is(deniedSys.error, 'path_not_allowed')
|
||||
|
||||
const proc = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'read_proc_file',
|
||||
args: { path: '/proc/bare_os/extra.json' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(proc.ok)
|
||||
t.ok(proc.json && proc.json.ok === true)
|
||||
|
||||
const seen = []
|
||||
ctx.execLine = async (cmd) => {
|
||||
seen.push(String(cmd))
|
||||
await vfs.writeFile('/tmp/agent.out', b4a.from('ok\n'))
|
||||
}
|
||||
const run = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'run_command',
|
||||
args: { command: 'git status && rm -rf /tmp/x' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(run.ok)
|
||||
t.ok(seen.length >= 1)
|
||||
|
||||
configRef.current.command_deny = ['rm -rf']
|
||||
const blocked = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'run_command',
|
||||
args: { command: 'rm -rf /tmp/x' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.absent(blocked.ok)
|
||||
t.is(blocked.error, 'command_denied')
|
||||
})
|
||||
|
||||
@@ -1,87 +1,48 @@
|
||||
/**
|
||||
* Mirrors semantics in lib/agent-helpers.js (apropos matching, mutate paths, proc allowlist).
|
||||
* Keep aligned when editing helpers.
|
||||
* Path / proc / apropos helpers (VM, same preamble as /bin/agent).
|
||||
*/
|
||||
import test from 'brittle'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
/** @type {readonly string[]} */
|
||||
const PROC_ALLOW = [
|
||||
'/proc/bare_os/metrics_live.json',
|
||||
'/proc/bare_os/features',
|
||||
'/proc/bare_os/features.json',
|
||||
'/proc/bare_os/swarm.json',
|
||||
'/proc/bare_os/capabilities.json',
|
||||
'/proc/bare_os/swarm_connection_manager_status.json'
|
||||
]
|
||||
const CODE = readFileSync(new URL('../lib/agent-helpers.js', import.meta.url), 'utf8')
|
||||
|
||||
function pathAllowedMutate(p) {
|
||||
const x = String(p || '').replace(/\\/g, '/')
|
||||
if (!x.startsWith('/') || x.includes('..')) return false
|
||||
return (
|
||||
x.startsWith('/home/') ||
|
||||
x.startsWith('/tmp/') ||
|
||||
x === '/tmp' ||
|
||||
x.startsWith('/root/') ||
|
||||
x.startsWith('/mnt/')
|
||||
)
|
||||
function load() {
|
||||
const sandbox = { TextDecoder, TextEncoder, Uint8Array, console }
|
||||
vm.createContext(sandbox)
|
||||
vm.runInContext(CODE, sandbox, { filename: 'agent-helpers.js' })
|
||||
return sandbox
|
||||
}
|
||||
|
||||
function procAllowed(p) {
|
||||
const x = String(p || '').replace(/\\/g, '/')
|
||||
if (!x.startsWith('/') || x.includes('..')) return false
|
||||
return PROC_ALLOW.includes(x)
|
||||
}
|
||||
|
||||
function aproposHits(db, needle, maxHits) {
|
||||
const n = String(needle || '').toLowerCase()
|
||||
const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200)
|
||||
if (!n || !db || typeof db !== 'object') return { lines: [], truncated: false }
|
||||
const d = /** @type {Record<string, unknown>} */ (db)
|
||||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||||
const apropos = Array.isArray(d.apropos) ? d.apropos : []
|
||||
const seen = new Set()
|
||||
/** @type {string[]} */
|
||||
const lines = []
|
||||
let truncated = false
|
||||
for (const row of apropos) {
|
||||
if (!row || typeof row !== 'object') continue
|
||||
const kw = typeof row.kw === 'string' ? row.kw : ''
|
||||
if (!kw.includes(n)) continue
|
||||
const idx = row.pageRef
|
||||
if (typeof idx !== 'number' || !pages[idx]) continue
|
||||
if (seen.has(idx)) continue
|
||||
seen.add(idx)
|
||||
const p = /** @type {Record<string, unknown>} */ (pages[idx])
|
||||
const name = typeof p.name === 'string' ? p.name : ''
|
||||
const sec = typeof p.section === 'number' ? p.section : 0
|
||||
const title = typeof p.title === 'string' ? p.title : ''
|
||||
lines.push(name + '(' + sec + ') - ' + title)
|
||||
if (lines.length >= max) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
lines.sort()
|
||||
return { lines, truncated }
|
||||
}
|
||||
|
||||
test('mutate paths allow guest areas only', async (t) => {
|
||||
t.ok(pathAllowedMutate('/home/guest/x'))
|
||||
t.ok(pathAllowedMutate('/tmp/a'))
|
||||
t.absent(pathAllowedMutate('/bin/foo'))
|
||||
t.absent(pathAllowedMutate('/etc/passwd'))
|
||||
t.absent(pathAllowedMutate('/proc/x'))
|
||||
test('mutate paths use a denylist (base system read-only, rest writable)', async (t) => {
|
||||
const s = load()
|
||||
t.ok(s.bareAgentPathAllowedMutate('/home/guest/x'))
|
||||
t.ok(s.bareAgentPathAllowedMutate('/tmp/a'))
|
||||
t.ok(s.bareAgentPathAllowedMutate('/var/log/app.log'))
|
||||
t.ok(s.bareAgentPathAllowedMutate('/opt/pkg/bin'))
|
||||
t.ok(s.bareAgentPathAllowedMutate('/mnt/extra/file'))
|
||||
t.ok(s.bareAgentPathAllowedMutate('/root/.ssh/config'))
|
||||
t.absent(s.bareAgentPathAllowedMutate('/bin/foo'))
|
||||
t.absent(s.bareAgentPathAllowedMutate('/etc/passwd'))
|
||||
t.absent(s.bareAgentPathAllowedMutate('/proc/x'))
|
||||
t.absent(s.bareAgentPathAllowedMutate('/usr/lib/x'))
|
||||
t.absent(s.bareAgentPathAllowedMutate('/'))
|
||||
})
|
||||
|
||||
test('proc allowlist exact paths', async (t) => {
|
||||
t.ok(procAllowed('/proc/bare_os/features'))
|
||||
t.ok(procAllowed('/proc/bare_os/swarm.json'))
|
||||
t.ok(procAllowed('/proc/bare_os/swarm_connection_manager_status.json'))
|
||||
t.absent(procAllowed('/proc/bare_os/other.json'))
|
||||
t.absent(procAllowed('/proc/../proc/bare_os/swarm.json'))
|
||||
test('reads allow any absolute path including kernel /proc', async (t) => {
|
||||
const s = load()
|
||||
t.ok(s.bareAgentPathAllowedRead('/home/guest/x'))
|
||||
t.ok(s.bareAgentPathAllowedRead('/bin/sh'))
|
||||
t.ok(s.bareAgentPathAllowedRead('/proc/bare_os/other.json'))
|
||||
t.ok(s.bareAgentProcReadPathAllowed('/proc/bare_os/other.json'))
|
||||
t.ok(s.bareAgentProcReadPathAllowed('/proc/bare_os/features'))
|
||||
t.absent(s.bareAgentPathAllowedRead('../etc/passwd'))
|
||||
t.absent(s.bareAgentProcReadPathAllowed('/home/guest/x'))
|
||||
t.absent(s.bareAgentProcReadPathAllowed('/proc/../proc/bare_os/swarm.json'))
|
||||
})
|
||||
|
||||
test('apropos substring matches kw like man -k', async (t) => {
|
||||
const s = load()
|
||||
const db = {
|
||||
pages: [
|
||||
{ name: 'grep', section: 1, title: 'pattern search' },
|
||||
@@ -93,7 +54,7 @@ test('apropos substring matches kw like man -k', async (t) => {
|
||||
{ kw: 'true', pageRef: 1 }
|
||||
]
|
||||
}
|
||||
const r = aproposHits(db, 'pat', 10)
|
||||
const r = s.bareAgentManAproposHits(db, 'pat', 10)
|
||||
t.is(r.lines.length, 1)
|
||||
t.ok(r.lines[0].startsWith('grep('))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user