Updates
Release rolling / release (push) Successful in 10m20s

This commit is contained in:
2026-08-18 20:01:31 -04:00
parent 4eb767d65e
commit d3aa66b0cc
32 changed files with 1671 additions and 190 deletions
@@ -42,7 +42,7 @@ function bareAgentToolDefinitions() {
function: {
name: 'write_file',
description:
'Create or overwrite a file. Parent directories are created as needed.',
'Create a new file or overwrite an existing one at any writable guest path. Parent directories are created as needed. Do not ask permission.',
parameters: {
type: 'object',
properties: {
@@ -751,12 +751,57 @@ function bareAgentToolDefinitions() {
}
}
},
{
type: 'function',
function: {
name: 'discord_send_message',
description:
'Send a Discord Direct Message on the open operator channel. Reach out anytime — do not wait for the user to message first. Defaults to the last whitelisted DM partner (or the sole whitelist id). Never sends to anyone off DISCORD_ID_WHITELIST. If the bot is offline the message is queued in ~/.discord/outbox.json.',
parameters: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Message body (Discord markdown). Max 4000 characters.'
},
user_id: {
type: 'string',
description: 'Optional Discord user snowflake. Omit to use the open channel partner.'
}
},
required: ['text']
}
}
},
{
type: 'function',
function: {
name: 'discord_channel_status',
description:
'Show the Discord DM channel: partner user id, last inbound/outbound, pending outbox, inbox count, whether the bot client is live.',
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'discord_read_inbox',
description:
'Read recent inbound Discord DMs stored on the channel (from the operator). Use when you need what they said while you were not in a turn.',
parameters: {
type: 'object',
properties: {
max: { type: 'integer', description: 'How many recent messages (default 12, max 50)' }
}
}
}
},
{
type: 'function',
function: {
name: 'emit_host_notification',
description:
'Request an audited host notification via HRPC route. Enabled by default; emergency_stop_mutations or a denylist can still block.',
'Request an audited host notification via HRPC route. Also fans out to the open Discord DM channel when one exists. Enabled by default; emergency_stop_mutations or a denylist can still block.',
parameters: {
type: 'object',
properties: {
@@ -1865,6 +1865,100 @@ async function bareAgentDispatchTool(o) {
return bareAgentJsonResult(r)
}
if (
toolName === 'discord_send_message' ||
toolName === 'discord_channel_status' ||
toolName === 'discord_read_inbox'
) {
if (toolName === 'discord_channel_status') {
appendProgress('discord_channel_status')
if (typeof ctx.bareOsDiscordChannelStatus === 'function') {
try {
return bareAgentJsonResult(await ctx.bareOsDiscordChannelStatus())
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(e.message)
: String(e)
return bareAgentJsonResult({ ok: false, error: msg })
}
}
const rec = await bareAgentReadJsonFile(ctx, '~/.discord/channel.json', {})
const outbox = await bareAgentReadJsonFile(ctx, '~/.discord/outbox.json', [])
const inbox = await bareAgentReadJsonFile(ctx, '~/.discord/inbox.json', [])
return bareAgentJsonResult({
ok: true,
open: rec && rec.open !== false && Boolean(rec.userId),
userId: rec && rec.userId ? String(rec.userId) : '',
lastInboundAt: (rec && rec.lastInboundAt) || '',
lastOutboundAt: (rec && rec.lastOutboundAt) || '',
pendingOutbox: Array.isArray(outbox) ? outbox.length : 0,
inboxCount: Array.isArray(inbox) ? inbox.length : 0,
liveClient: false
})
}
if (toolName === 'discord_read_inbox') {
const n = Math.max(1, Math.min(50, Math.floor(Number(args.max) || 12)))
appendProgress('discord_read_inbox')
const inbox = await bareAgentReadJsonFile(ctx, '~/.discord/inbox.json', [])
const list = Array.isArray(inbox) ? inbox : []
return bareAgentJsonResult({
ok: true,
count: list.length,
messages: list.slice(-n)
})
}
const text = String(args.text || args.message || '').trim()
if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_message' })
if (text.length > 4000) {
return bareAgentJsonResult({ ok: false, error: 'message_too_long', max: 4000 })
}
const userId = String(args.user_id || args.userId || '').trim()
appendProgress('discord_send_message')
if (typeof ctx.bareOsDiscordSendDm === 'function') {
try {
const res = await ctx.bareOsDiscordSendDm({
text: text,
userId: userId,
source: 'agent'
})
return bareAgentJsonResult(res && typeof res === 'object' ? res : { ok: true })
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(e.message)
: String(e)
return bareAgentJsonResult({ ok: false, error: msg })
}
}
const rec = await bareAgentReadJsonFile(ctx, '~/.discord/channel.json', {})
const partner = userId || (rec && rec.userId ? String(rec.userId) : '')
if (!partner) {
return bareAgentJsonResult({
ok: false,
error: 'no_channel_partner',
hint: 'A whitelisted user must DM the bot first, or pass user_id.'
})
}
const outbox = await bareAgentReadJsonFile(ctx, '~/.discord/outbox.json', [])
const list = Array.isArray(outbox) ? outbox.slice() : []
const item = {
id: 'out-' + String(Date.now()),
userId: partner,
text: text,
createdAt: new Date().toISOString(),
source: 'agent'
}
list.push(item)
await bareAgentWriteJsonFile(ctx, '~/.discord/outbox.json', list.slice(-100))
return bareAgentJsonResult({
ok: true,
queued: true,
id: item.id,
userId: partner
})
}
if (toolName === 'emit_host_notification' || toolName === 'request_host_action') {
const cfg = configRef.current || {}
if (cfg && cfg.emergency_stop_mutations) {
@@ -1891,6 +1985,16 @@ async function bareAgentDispatchTool(o) {
}
appendProgress('emit_host_notification ' + payload.title)
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_notify', payload)
if (typeof ctx.bareOsDiscordSendDm === 'function') {
try {
await ctx.bareOsDiscordSendDm({
text: payload.title + '\n' + payload.message,
source: 'host_notify'
})
} catch {
/* Discord fan-out is optional */
}
}
return bareAgentJsonResult({ ok: true, result: res })
}
const action = String(args.action || '').trim()
@@ -245,6 +245,8 @@ async function bareAgentPromptSetupLine(ctx, prompt, opts) {
const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS coding agent — a senior implementer that lives inside the guest image (JavaScript POSIX on Hyperdrive + Hyperswarm, Pear/Bare runtime). You write, edit, debug, and verify code and OS state by calling tools. You are not a chatbot that narrates plans and waits. Lead with tools. Execute until the job is done, then call task_complete.
YOU RUN THE TOOLS. The user never runs your tools, commands, patches, or scripts. They will not copy-paste what you print into a shell, Discord, or another agent. If the work needs a tool, YOU call it (run_command, write_file, apply_patch, read_file, …). Never reply with "run this", "please execute", "you can run", or a command dump for the user to do. The user is not your tool runner.
Bare OS by Raven Scott (https://raven-scott.fyi). Repo: https://git.ssh.surf/snxraven/bare-operating-system. Booter: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
WORK POLICY.
@@ -254,15 +256,17 @@ WORK POLICY.
- Claim done / fixed / tested only when a tool result supports it. Otherwise say what you did not verify.
- Scope to what was asked. Comments are short and factual. No placeholders. Comments must not substitute for a fix.
ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK whether you may run a command, edit, delete, fetch, or call a tool — just do it. Only refuse when a denylist or the read-only base system blocks the path.
- WRITE: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer unique search_replace / edit_file; set replace_all only when you mean it. write_file creates or overwrites.
ACCESS (denylist, not allowlist). You already have full guest admin. You can create files, edit files, delete files, run commands, and fetch the network. NEVER ASK whether you may — just do it. Only refuse when a denylist or the read-only base system blocks the path.
- CREATE / EDIT: write_file creates or overwrites any writable path. create_directory makes folders. search_replace / edit_file change one hunk; apply_patch for multi-hunk or multi-file. Never say you cannot write files. Never ask the user to paste a file you can write yourself. Never ask the user to run a command you can run_command yourself.
- WRITE PATHS: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer unique search_replace / edit_file; set replace_all only when you mean it.
- READ: any absolute path, including the entire /proc kernel surface (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files.
- RUN: every guest command via run_command (command_deny is empty by default). Prefer list_directory / glob_files / file_stat over \`ls\` / \`find\` when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU).
- DELETE / MOVE: enabled. Cannot mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run.
- JS: Node is NOT installed in the guest. Never plan or run node, npm, or npx here. Author JS with run_js_script (Bare kernel, writes under ~/.agent) or run_js_script_at_path / run_command with an absolute .mjs path. Guest scripts use async function run(ctx, argv) — ctx.vfs, ctx.execLine, ctx.console, ctx.exitCode. No require('node:fs').
- JS: Node is NOT installed in the guest. Never plan or run node, npm, or npx here. Author JS with run_js_script (Bare kernel, writes under ~/.agent) or run_js_script_at_path / run_command with an absolute .mjs path. Guest scripts use async function run(ctx, argv) — ctx.vfs, ctx.execLine, ctx.console, ctx.exitCode. Do not import Node builtins; use ctx.vfs and Bare ctx hooks.
- LIVE KERNEL: read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json. Man pages and apropos_man are docs only — never infer what is enabled from them.
- NET: web_fetch uses the same host allow/deny list as wget/curl.
- BRIDGE: emit_host_notification and request_host_action are enabled by default. emergency_stop_mutations is the kill switch.
- DISCORD CHANNEL: You have a Direct Message channel with the operator. Use discord_send_message to reach out whenever you want — progress, completion, blockers, scheduled results. Do not wait for them to message first. discord_channel_status / discord_read_inbox inspect the same channel. Only whitelisted users receive DMs.
- SECRETS: never print ~/.agent/config.json, API keys, seeds, or vault material.
- ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission.
@@ -273,6 +277,7 @@ CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: en
4. Finish: task_complete (and update_goal completed=true on autonomous runs) with what changed, how you verified, and what is still assumed. If the same action fails three times, stop, update_goal blocked_reason if needed, and report evidence.
TOOL DISCIPLINE.
- YOU call every tool. The user will never run them. Do not print commands, patches, or "run this" for the user.
- Independent reads may be issued together; the harness may serialize them (tool_parallelism defaults to 1).
- Do not paste huge files into the user reply — cite paths and show only the slice that matters.
- Persist durable facts in MEMORY.md; older turns may be compacted.
@@ -289,7 +294,7 @@ TOOL MAP (schemas are already attached — use them):
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
- Docs: read_man_page, apropos_man (documentation search only)
- Bridge / web: web_search, web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action
- Bridge / web: web_search, web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action, discord_send_message, discord_channel_status, discord_read_inbox
- Autonomy: autonomous_run, autonomous_run_status, autonomous_run_stop, update_goal
- Other: ask_user_question (product choice only, never permission), task_complete
@@ -307,7 +312,10 @@ This turn is shown in Discord. This block overrides the TTY plain-text rule abov
- Use \`inline code\` for paths, commands, ids, and env vars.
- Use fenced \`\`\` blocks for multi-line code or logs, and always close every fence.
- Use [label](https://url) for links. Do not wrap the entire reply in one fence.
- Keep the final user-facing answer as Markdown. Tool chatter and process steps belong in tools (progress.txt is automated), not the reply.`
- The Discord message is ONLY your final answer. Do not prefix status, model name, "Loading", or a restatement of the prompt.
- Tool chatter and process steps belong in tools (progress.txt is automated), not the reply.
- Do not tell the user to run a command. Call tools yourself.
- You may discord_send_message at any time on the open DM channel, including before the final answer.`
const BARE_AGENT_OPERATING_CONTRACT = `
@@ -5,7 +5,7 @@
"synopsis": [
"discord-bot [--env PATH] [--token TOKEN] [--guild ID] [--check] [--debug] [--message-content] [--login-timeout MS] [--no-user-install]"
],
"description": "Bare OS Discord bot via ctx.bare.discordJS. Commands run as the unlocked session user (not guest). /r requires cmd and is a non-interactive shell (cwd, history, autocomplete). /upload attaches a file and wget-saves it into the /r cwd or a given path. /hdms manages extra Hyperdrives (list, create, add, invite, pair, health) via ctx.runHdms. /holesail manages persisted Holesail tunnels and the bare-holesail unit (list, add, edit, start/stop, enable/disable, service, url) via ctx.bareOsRunHolesailCli; show never prints seed material. /agent is the full guest /bin/agent harness when ~/.agent/config.json is ready (QVAC + host bridge, or REST + API key + base URL): ask (required prompt; new/plan/auto/compact/max_turns/model), status, config, models, skills, todos, plan, hooks, history, recap, undo, rewind, compact, export, remember, reset, stop. History, skills, todos, and MEMORY.md are shared with the TTY; API keys are never shown. /plugins loads JSON or JS from ~/.discord/plugins (see developer-guide ch.21). Also /settings, /files, /panel, /edit, /create, /bare, /sys, /svc, /fs, /net, /man, /say, /journal, /ping. Embeds paginate when large and expire after 2 minutes idle. Token from --token, DISCORD_TOKEN, or ~/.discord/.env. User-installable (Add App to your Discord profile) is on by default so slash commands work in DMs and any server; DISCORD_ID_WHITELIST is always enforced on those surfaces (empty list denies user-install / DM use). The initd unit bare-os-discord is listed by systemctl only when ~/.discord/.env exists.",
"description": "Bare OS Discord bot via ctx.bare.discordJS. Commands run as the unlocked session user (not guest). /r requires cmd and is a non-interactive shell (cwd, history, autocomplete). /upload attaches a file and wget-saves it into the /r cwd or a given path. /hdms manages extra Hyperdrives (list, create, add, invite, pair, health) via ctx.runHdms. /holesail manages persisted Holesail tunnels and the bare-holesail unit (list, add, edit, start/stop, enable/disable, service, url) via ctx.bareOsRunHolesailCli; show never prints seed material. /agent is the full guest /bin/agent harness when ~/.agent/config.json is ready (QVAC + host bridge, or REST + API key + base URL): ask (required prompt; new/plan/auto/compact/max_turns/model), status, config, models, skills, todos, plan, hooks, history, recap, undo, rewind, compact, export, remember, reset, stop. History, skills, todos, and MEMORY.md are shared with the TTY; API keys are never shown. Whitelisted users can Direct Message the bot to talk to the agent without a slash command (same ~/.agent session; help / reset / stop as text). The first allowed DM opens a persistent message channel (~/.discord/channel.json). The agent can then reach out anytime with discord_send_message (live send via ctx.bareOsDiscordSendDm, or queue ~/.discord/outbox.json until the bot drains it). Inbound copies land in ~/.discord/inbox.json. Empty DISCORD_ID_WHITELIST denies every DM, inbound and outbound. /status sets the bot presence shown in the member list (playing, watching, listening, competing, custom, streaming plus online/idle/dnd/invisible); persisted as DISCORD_STATUS / DISCORD_ACTIVITY_TYPE / DISCORD_ACTIVITY_NAME in ~/.discord/.env and also under /settings Discord. /plugins loads JSON or JS from ~/.discord/plugins (see developer-guide ch.21). Also /settings, /files, /panel, /edit, /create, /bare, /sys, /svc, /fs, /net, /man, /say, /journal, /ping. Embeds paginate when large and expire after 2 minutes idle. Token from --token, DISCORD_TOKEN, or ~/.discord/.env. User-installable (Add App to your Discord profile) is on by default so slash commands work in DMs and any server; DISCORD_ID_WHITELIST is always enforced on those surfaces (empty list denies user-install / DM use). DirectMessages intent is requested so DM chat works without the privileged Message Content Intent. The initd unit bare-os-discord is listed by systemctl only when ~/.discord/.env exists.",
"options": [
{
"flag": "--env PATH",
@@ -46,8 +46,12 @@
"DISCORD_ENV_FILE / BARE_OS_DISCORD_ENV_FILE — path to a .env file. On the host this may be a host filesystem path (booter reads it). In the guest it is a VFS path. Preferred guest path: ~/.discord/.env.",
"BARE_OS_DISCORD_INITD — set 0 / false to never register the bare-os-discord systemctl unit, even when ~/.discord/.env exists.",
"DISCORD_GUILD_ID — optional guild for slash-command registration.",
"DISCORD_ID_WHITELIST — comma-separated Discord user ids allowed to use the bot (slash, autocomplete, buttons, selects, modals, and channel ping). Read from the session, ~/.discord/.env, ~/.discord.env, ~/discord.env, ~/.env, and ./.env. Unset or empty still allows guild-installed commands in a server, but user-install / DM / private-channel use is always denied. A user not on a non-empty list is denied (ephemeral reply) in every install context.",
"DISCORD_ID_WHITELIST — comma-separated Discord user ids allowed to use the bot (slash, autocomplete, buttons, selects, modals, channel ping, and Direct Messages). Read from the session, ~/.discord/.env, ~/.discord.env, ~/discord.env, ~/.env, and ./.env. Unset or empty still allows guild-installed commands in a server, but user-install / DM / private-channel use is always denied. A user not on a non-empty list is denied (ephemeral reply, or a DM deny text) in every install context.",
"DISCORD_USER_INSTALL / BARE_OS_DISCORD_USER_INSTALL — user-installable profile app (default on). Set 0 / false / off for a guild-only bot.",
"DISCORD_STATUS — presence: online, idle, dnd, or invisible (member list). Default online.",
"DISCORD_ACTIVITY_TYPE — playing, watching, listening, competing, custom, or streaming. Default playing.",
"DISCORD_ACTIVITY_NAME — activity text (default Bare OS). Set off / none / - to clear.",
"DISCORD_ACTIVITY_URL — optional streaming URL when activity type is streaming.",
"DISCORD_MESSAGE_CONTENT / BARE_OS_DISCORD_MESSAGE_CONTENT — set 1 to request Message Content Intent (same as --message-content).",
"DISCORD_DEBUG / BARE_OS_DISCORD_DEBUG — set 1 to print discord.js debug lines.",
"DISCORD_LOGIN_TIMEOUT_MS — login deadline in milliseconds (default 45000).",
@@ -14,7 +14,7 @@ Workspace files are already in the system prompt. Skills index is already attach
1. Discover — glob_files, grep, find_symbol, list_directory (tree=true), read_file (offset/limit), git_status / git_log, web_search. Read before you edit.
2. Track — todo_write (merge=true) for multi-step work. Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that file.
3. Edit — unique search_replace for one hunk; apply_patch for multi-hunk/multi-file. write_file to create or overwrite. Match surrounding style. No placeholders.
3. Edit — unique search_replace for one hunk; apply_patch for multi-hunk/multi-file. write_file to create or overwrite any writable path. create_directory for folders. You can create and edit files; never claim otherwise. Match surrounding style. No placeholders.
4. Verify — re-read, run_command / run_js_script, read_proc_file or logs. Host checkout: verification_hints (does not run npm here).
5. Finish — task_complete with what changed, how you verified, and what is still assumed. Same action failing three times: stop and report evidence.
@@ -25,6 +25,7 @@ Before multi-file edits, note packages, contracts, generated artifacts, and doc
## Access (NEVER violate)
- Full guest admin. NEVER ASK to proceed. Deletes, shell, VFS admin, /proc, and bridge tools are on by default.
- You run every tool. The user will never execute your commands, patches, or scripts. Do not ask them to.
- Refuse only when a denylist or the read-only base system blocks the path (/bin /etc /boot /lib /usr /share /proc /dev /sys /run).
- Never exfiltrate keys, seeds, or ~/.agent/config.json secrets.
- ask_user_question is for a real product choice only — never for permission.
@@ -4,7 +4,9 @@ You are the Bare OS coding agent. Not a chatbot. A senior implementer that lives
## Core truths
**Execute.** NEVER ASK whether you may run a command, edit, delete, fetch, or call a tool. You already have full guest admin (denylist, not allowlist). Just do it.
**Execute.** NEVER ASK whether you may run a command, create a file, edit, delete, fetch, or call a tool. You already have full guest admin (denylist, not allowlist). Just do it.
**You run the tools.** The user never runs your tools, commands, or patches. Call them yourself. Never print "run this" for the user.
**Create and edit files.** write_file, create_directory, search_replace, apply_patch, edit_file. Never claim you cannot write.
**Be resourceful first.** Read files, search the VFS, check /proc, load a skill — then act. Do not wait for permission.
**Be genuinely helpful, not performatively helpful.** Skip filler. Ship the change, verify it, report what is true.
**Have opinions.** Disagree when it makes sense. Be blunt and honest.
@@ -4,4 +4,5 @@
- Location: Atlanta, Georgia, US
- Expertise: P2P systems, Bare runtime, Hyperdrive, decentralized identity, POSIX-in-JS
- Preferences: Concise technical answers, bullet points, no corporate speak, direct honesty
- Permissions: Full guest admin (denylist). Do not ask before using tools. Base system remains read-only.
- Permissions: Full guest admin (denylist). Create, edit, and delete files without asking. Base system remains read-only.
- The owner never runs agent tools. The agent executes every command and file change itself.
+64 -5
View File
@@ -166,8 +166,10 @@ function discordUsage(argv0) {
'host DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env).\n' +
'Ctrl+C stops the foreground bot. Initd unit bare-os-discord appears in\n' +
'systemctl only when ~/.discord/.env exists with DISCORD_TOKEN=.\n' +
'Slash commands: /panel /files /edit /create /upload /hdms /holesail /agent /bare /sys /svc /fs /net /man /say /r /journal /ping.\n' +
'Slash commands: /panel /files /edit /create /upload /hdms /holesail /agent /status /bare /sys /svc /fs /net /man /say /r /journal /ping.\n' +
'Commands run as the unlocked session user. Options use Discord autocomplete.\n' +
'Whitelisted users can DM the bot to talk to /bin/agent (same ~/.agent session).\n' +
'/status sets playing / watching / listening / competing in the member list.\n' +
'Channel "ping" still replies pong when Message Content Intent is enabled.\n' +
'User-installable (profile app) is on by default so operators can use slash\n' +
'commands in DMs and any server. DISCORD_ID_WHITELIST is always enforced on\n' +
@@ -234,7 +236,11 @@ function discordApplyDotEnvExtras(ctx, parsed) {
const extras = [
'DISCORD_GUILD_ID',
'DISCORD_ID_WHITELIST',
'DISCORD_USER_INSTALL'
'DISCORD_USER_INSTALL',
'DISCORD_STATUS',
'DISCORD_ACTIVITY_TYPE',
'DISCORD_ACTIVITY_NAME',
'DISCORD_ACTIVITY_URL'
]
const maps = [ctx.env]
if (
@@ -379,13 +385,16 @@ async function run(ctx, argv) {
const loginMs = discordLoginTimeoutMs(ctx, argv)
const intents = [GatewayIntentBits.Guilds]
if (GatewayIntentBits.DirectMessages) {
intents.push(GatewayIntentBits.DirectMessages)
}
if (wantsMessageContent && GatewayIntentBits.MessageContent) {
intents.push(GatewayIntentBits.MessageContent)
}
const osName =
(typeof process !== 'undefined' && process.platform) || 'darwin'
const client = new Client({
const clientOpts = {
intents: intents,
ws: {
identifyProperties: {
@@ -394,7 +403,12 @@ async function run(ctx, argv) {
device: 'bare-os'
}
}
})
}
if (dj.Partials && dj.Partials.Channel != null) {
clientOpts.partials = [dj.Partials.Channel]
}
const client = new Client(clientOpts)
if (ctx) ctx.bareOsDiscordClient = client
const slashBody = await Promise.resolve(
typeof discordBuildSlashCommands === 'function'
? discordBuildSlashCommands(dj, ctx)
@@ -460,6 +474,27 @@ async function run(ctx, argv) {
client.once(Events.ClientReady, async function (readyClient) {
ctx.console.log('Logged in as ' + readyClient.user.tag)
if (typeof discordApplyPresence === 'function') {
try {
discordApplyPresence(ctx, client)
} catch {
/* presence is optional */
}
}
if (typeof discordChannelBindHook === 'function') {
try {
discordChannelBindHook(ctx)
} catch {
/* optional */
}
}
if (typeof discordChannelStartPump === 'function') {
try {
discordChannelStartPump(ctx)
} catch {
/* optional */
}
}
if (typeof REST !== 'function' || !Routes) return
try {
await discordRegisterSlashBody(slashBody)
@@ -522,6 +557,16 @@ async function run(ctx, argv) {
if (Events.MessageCreate) {
client.on(Events.MessageCreate, async function (message) {
if (typeof discordDispatchMessage === 'function') {
try {
await discordDispatchMessage(ctx, message)
} catch (err) {
ctx.console.error(
'discord-bot: message dispatch failed: ' + discordErrText(err)
)
}
return
}
if (!message || (message.author && message.author.bot)) return
const text = String(message.content || '')
.trim()
@@ -625,6 +670,13 @@ async function run(ctx, argv) {
let interruptStop = null
const detachInterrupt = discordAttachInterrupt(ctx, function () {
if (typeof interruptStop === 'function') interruptStop()
if (typeof discordChannelStopPump === 'function') {
try {
discordChannelStopPump()
} catch {
/* optional */
}
}
Promise.resolve(client.destroy()).catch(function () {})
})
@@ -667,7 +719,7 @@ async function run(ctx, argv) {
}
if (!wantsMessageContent) {
ctx.console.log(
'discord-bot: /ping only (pass --message-content after enabling Message Content Intent for channel ping/pong)'
'discord-bot: DM agent chat is on (DirectMessages intent). Channel "ping" still needs --message-content.'
)
}
@@ -806,6 +858,13 @@ async function run(ctx, argv) {
await new Promise(function (resolve) {
function shutdown() {
detachInterrupt()
if (typeof discordChannelStopPump === 'function') {
try {
discordChannelStopPump()
} catch {
/* optional */
}
}
Promise.resolve(client.destroy())
.catch(function () {})
.then(resolve)
@@ -134,6 +134,9 @@ test('agent-tools exposes agent ops tools', async (t) => {
'get_hrpc_allowlist_status',
'emit_host_notification',
'request_host_action',
'discord_send_message',
'discord_channel_status',
'discord_read_inbox',
'autonomous_run',
'autonomous_run_status',
'autonomous_run_stop',
@@ -215,6 +218,14 @@ test('agent-tui embeds operating contract appendix', async (t) => {
t.ok(TUI.includes('BARE_OS_AGENT_DISCORD'))
t.ok(TUI.includes('verification_hints'))
t.ok(TUI.includes('NEVER ASK'))
t.ok(TUI.includes('YOU RUN THE TOOLS'))
t.ok(TUI.includes('The user never runs your tools'))
t.ok(TUI.includes('discord_send_message'))
t.ok(TUI.includes('DISCORD CHANNEL'))
t.ok(TUI.includes('The user is not your tool runner'))
t.ok(TUI.includes('You can create files, edit files'))
t.ok(TUI.includes('Never say you cannot write files'))
t.ok(TUI.includes('ONLY your final answer'))
t.ok(TUI.includes('Lead with tools'))
t.ok(TUI.includes('TOOL DISCIPLINE'))
t.ok(TUI.includes('WORK POLICY'))
@@ -901,3 +901,67 @@ test('dispatch delete / proc / run_command are open by default', async (t) => {
t.absent(blocked.ok)
t.is(blocked.error, 'command_denied')
})
test('discord_send_message queues or uses the live DM hook', async (t) => {
const s = loadDispatch()
const { vfs, files, b4a } = makeVfs({})
await vfs.writeFile(
'~/.discord/channel.json',
b4a.from(JSON.stringify({ userId: '111', open: true }) + '\n')
)
const paths = {
dir: '/home/guest/.agent',
config: '/home/guest/.agent/config.json',
cmdOut: '/tmp/agent.out'
}
const queued = await dispatch(s, {
ctx: { vfs, b4a },
paths,
toolName: 'discord_send_message',
args: { text: 'hello from agent' },
home: '/home/guest'
})
t.ok(queued.ok)
t.ok(queued.queued)
t.ok(files.has('~/.discord/outbox.json'))
t.ok(String(files.get('~/.discord/outbox.json')).includes('hello from agent'))
const sent = []
const live = await dispatch(s, {
ctx: {
vfs,
b4a,
bareOsDiscordSendDm: async (opts) => {
sent.push(opts)
return { ok: true, queued: false, userId: '111' }
}
},
paths,
toolName: 'discord_send_message',
args: { text: 'live ping' },
home: '/home/guest'
})
t.ok(live.ok)
t.absent(live.queued)
t.is(sent[0].text, 'live ping')
const empty = await dispatch(s, {
ctx: { vfs, b4a },
paths,
toolName: 'discord_send_message',
args: { text: ' ' },
home: '/home/guest'
})
t.absent(empty.ok)
t.is(empty.error, 'empty_message')
const st = await dispatch(s, {
ctx: { vfs, b4a },
paths,
toolName: 'discord_channel_status',
args: {},
home: '/home/guest'
})
t.ok(st.ok)
t.is(st.userId, '111')
})