refactor: shared source — replace per-group agent-runner copies with single RO mount
Replace the per-group agent-runner-src copy model with a single shared read-only mount. Source and skills are now RO + shared; personality, config, working files, and Claude state stay RW + per-group. Key changes: - Mount container/agent-runner/src/ RO at /app/src (all groups share one copy) - Mount container/skills/ RO at /app/skills; per-group skill selection via symlinks in .claude-shared/skills/ based on container.json "skills" field - Mount container.json as nested RO bind on top of RW group dir - Move all NANOCLAW_* env vars to container.json (runner reads at startup) - New runner config.ts module replaces process.env reads - Move command gate (filtered/admin) from container to host router - Dockerfile: remove source COPY, split CLI installs (claude-code last), move agent-runner deps above CLIs for better layer caching - Add writeOutboundDirect for router denial responses - Design doc at docs/shared-src.md Not included (follow-up): DB migration to drop agent_provider columns, cleanup of orphaned agent-runner-src directories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
* for policy refusals.
|
||||
*/
|
||||
import { getChannelAdapter } from './channels/channel-registry.js';
|
||||
import { gateCommand } from './command-gate.js';
|
||||
import { getAgentGroup } from './db/agent-groups.js';
|
||||
import { recordDroppedMessage } from './db/dropped-messages.js';
|
||||
import {
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
import { findSessionForAgent } from './db/sessions.js';
|
||||
import { startTypingRefresh } from './modules/typing/index.js';
|
||||
import { log } from './log.js';
|
||||
import { resolveSession, writeSessionMessage } from './session-manager.js';
|
||||
import { resolveSession, writeSessionMessage, writeOutboundDirect } from './session-manager.js';
|
||||
import { wakeContainer } from './container-runner.js';
|
||||
import { getSession } from './db/sessions.js';
|
||||
import type { AgentGroup, MessagingGroup, MessagingGroupAgent } from './types.js';
|
||||
@@ -398,6 +399,29 @@ async function deliverToAgent(
|
||||
threadId: event.threadId,
|
||||
};
|
||||
|
||||
// Command gate: classify slash commands before they reach the container.
|
||||
// Filtered commands are dropped silently. Denied admin commands get a
|
||||
// permission-denied response written directly to messages_out.
|
||||
if (event.message.kind === 'chat' || event.message.kind === 'chat-sdk') {
|
||||
const gate = gateCommand(event.message.content, userId, agent.agent_group_id);
|
||||
if (gate.action === 'filter') {
|
||||
log.debug('Filtered command dropped by gate', { agentGroupId: agent.agent_group_id });
|
||||
return;
|
||||
}
|
||||
if (gate.action === 'deny') {
|
||||
writeOutboundDirect(session.agent_group_id, session.id, {
|
||||
id: `deny-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
kind: 'chat',
|
||||
platformId: deliveryAddr.platformId,
|
||||
channelType: deliveryAddr.channelType,
|
||||
threadId: deliveryAddr.threadId,
|
||||
content: JSON.stringify({ text: `Permission denied: ${gate.command} requires admin access.` }),
|
||||
});
|
||||
log.info('Admin command denied by gate', { command: gate.command, userId, agentGroupId: agent.agent_group_id });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeSessionMessage(session.agent_group_id, session.id, {
|
||||
id: messageIdForAgent(event.message.id, agent.agent_group_id),
|
||||
kind: event.message.kind,
|
||||
|
||||
Reference in New Issue
Block a user