Agent Updates

This commit is contained in:
2026-08-18 14:28:02 -04:00
parent 154c13d902
commit c6d4836793
28 changed files with 1000 additions and 468 deletions
+71 -21
View File
@@ -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/')))
}
+71 -11
View File
@@ -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 }
}
/**
+102 -69
View File
@@ -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 ?? '')
}
+4 -4
View File
@@ -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
+3 -1
View File
@@ -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('))
})