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:
exe.dev user
2026-04-21 12:05:19 +00:00
committed by gavrielc
parent 596035be09
commit 8a12fa61ac
14 changed files with 715 additions and 249 deletions

View File

@@ -7,6 +7,7 @@
* The container never writes to inbound.db — all status tracking goes through
* processing_ack. The host reads processing_ack to sync message lifecycle.
*/
import { getConfig } from '../config.js';
import { getInboundDb, getOutboundDb } from './connection.js';
export interface MessageInRow {
@@ -26,14 +27,16 @@ export interface MessageInRow {
content: string;
}
// Cap on how many messages reach the agent in one prompt, including any
// accumulated-but-not-triggered context. Host controls the cap via the
// NANOCLAW_MAX_MESSAGES_PER_PROMPT env var; default mirrors the host's
// config.ts default of 10.
const MAX_MESSAGES_PER_PROMPT = Math.max(
1,
parseInt(process.env.NANOCLAW_MAX_MESSAGES_PER_PROMPT || '10', 10) || 10,
);
// Cap on how many messages reach the agent in one prompt. Read from
// container.json; falls back to 10.
function getMaxMessagesPerPrompt(): number {
try {
return getConfig().maxMessagesPerPrompt;
} catch {
// Config not loaded yet (e.g. test harness) — use default
return 10;
}
}
/**
* Fetch pending messages that are due for processing.
@@ -58,7 +61,7 @@ export function getPendingMessages(): MessageInRow[] {
ORDER BY seq DESC
LIMIT ?`,
)
.all(MAX_MESSAGES_PER_PROMPT) as MessageInRow[];
.all(getMaxMessagesPerPrompt()) as MessageInRow[];
if (pending.length === 0) return [];