feat: remove container layer, run agents as direct host processes

- Rewrite container-runner.ts to spawn node processes directly instead
  of Docker/Apple Container
- Runner paths configurable via env vars (NANOCLAW_GROUP_DIR, etc.)
  with container-path defaults for backwards compat
- Pass real credentials directly (no credential proxy needed)
- Remove ensureContainerSystemRunning() and startCredentialProxy()
  from startup
- Add build:runners script for building agent-runner and codex-runner
- Fix TS strict mode errors in agent-runner ipc-mcp-stdio.ts
This commit is contained in:
Eyejoker
2026-03-10 21:05:21 +09:00
parent 4a34a9c802
commit 08dd468692
15 changed files with 449 additions and 498 deletions

View File

@@ -161,9 +161,7 @@ function createMessage(overrides: {
member: overrides.memberDisplayName
? { displayName: overrides.memberDisplayName }
: null,
guild: overrides.guildName
? { name: overrides.guildName }
: null,
guild: overrides.guildName ? { name: overrides.guildName } : null,
channel: {
name: overrides.channelName ?? 'general',
messages: {
@@ -646,8 +644,11 @@ describe('DiscordChannel', () => {
await channel.sendMessage('dc:1234567890123456', 'Hello');
const fetchedChannel = await currentClient().channels.fetch('1234567890123456');
expect(currentClient().channels.fetch).toHaveBeenCalledWith('1234567890123456');
const fetchedChannel =
await currentClient().channels.fetch('1234567890123456');
expect(currentClient().channels.fetch).toHaveBeenCalledWith(
'1234567890123456',
);
});
it('strips dc: prefix from JID', async () => {

View File

@@ -1,4 +1,10 @@
import { Client, Events, GatewayIntentBits, Message, TextChannel } from 'discord.js';
import {
Client,
Events,
GatewayIntentBits,
Message,
TextChannel,
} from 'discord.js';
import { ASSISTANT_NAME, TRIGGER_PATTERN } from '../config.js';
import { readEnvFile } from '../env.js';
@@ -27,7 +33,11 @@ export class DiscordChannel implements Channel {
private typingIntervals = new Map<string, NodeJS.Timeout>();
private agentTypeFilter?: AgentType;
constructor(botToken: string, opts: DiscordChannelOpts, agentTypeFilter?: AgentType) {
constructor(
botToken: string,
opts: DiscordChannelOpts,
agentTypeFilter?: AgentType,
) {
this.botToken = botToken;
this.opts = opts;
this.agentTypeFilter = agentTypeFilter;
@@ -95,18 +105,20 @@ export class DiscordChannel implements Channel {
// Handle attachments — store placeholders so the agent knows something was sent
if (message.attachments.size > 0) {
const attachmentDescriptions = [...message.attachments.values()].map((att) => {
const contentType = att.contentType || '';
if (contentType.startsWith('image/')) {
return `[Image: ${att.name || 'image'}]`;
} else if (contentType.startsWith('video/')) {
return `[Video: ${att.name || 'video'}]`;
} else if (contentType.startsWith('audio/')) {
return `[Audio: ${att.name || 'audio'}]`;
} else {
return `[File: ${att.name || 'file'}]`;
}
});
const attachmentDescriptions = [...message.attachments.values()].map(
(att) => {
const contentType = att.contentType || '';
if (contentType.startsWith('image/')) {
return `[Image: ${att.name || 'image'}]`;
} else if (contentType.startsWith('video/')) {
return `[Video: ${att.name || 'video'}]`;
} else if (contentType.startsWith('audio/')) {
return `[Audio: ${att.name || 'audio'}]`;
} else {
return `[File: ${att.name || 'file'}]`;
}
},
);
if (content) {
content = `${content}\n${attachmentDescriptions.join('\n')}`;
} else {
@@ -132,7 +144,13 @@ export class DiscordChannel implements Channel {
// Store chat metadata for discovery
const isGroup = message.guild !== null;
this.opts.onChatMetadata(chatJid, timestamp, chatName, 'discord', isGroup);
this.opts.onChatMetadata(
chatJid,
timestamp,
chatName,
'discord',
isGroup,
);
// Only deliver full message for registered groups matching our agent type
const group = this.opts.registeredGroups()[chatJid];
@@ -143,7 +161,10 @@ export class DiscordChannel implements Channel {
);
return;
}
if (this.agentTypeFilter && (group.agentType || 'claude-code') !== this.agentTypeFilter) {
if (
this.agentTypeFilter &&
(group.agentType || 'claude-code') !== this.agentTypeFilter
) {
return; // This JID belongs to a different agent type's bot
}
@@ -278,14 +299,22 @@ registerChannel('discord', (opts: ChannelOpts) => {
return null;
}
// If a second Codex bot token exists, this instance only handles claude-code groups
const hasCodexBot = !!(process.env.DISCORD_CODEX_BOT_TOKEN || envVars.DISCORD_CODEX_BOT_TOKEN);
return new DiscordChannel(token, opts, hasCodexBot ? 'claude-code' : undefined);
const hasCodexBot = !!(
process.env.DISCORD_CODEX_BOT_TOKEN || envVars.DISCORD_CODEX_BOT_TOKEN
);
return new DiscordChannel(
token,
opts,
hasCodexBot ? 'claude-code' : undefined,
);
});
registerChannel('discord-codex', (opts: ChannelOpts) => {
const envVars = readEnvFile(['DISCORD_CODEX_BOT_TOKEN']);
const token =
process.env.DISCORD_CODEX_BOT_TOKEN || envVars.DISCORD_CODEX_BOT_TOKEN || '';
process.env.DISCORD_CODEX_BOT_TOKEN ||
envVars.DISCORD_CODEX_BOT_TOKEN ||
'';
if (!token) return null; // Codex Discord bot is optional
return new DiscordChannel(token, opts, 'codex');
});