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

@@ -31,8 +31,7 @@ let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
/** Inbound DB — container opens read-only (host is the sole writer). */
export function getInboundDb(): Database {
if (!_inbound) {
const dbPath = process.env.SESSION_INBOUND_DB_PATH || DEFAULT_INBOUND_PATH;
_inbound = new Database(dbPath, { readonly: true });
_inbound = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
_inbound.exec('PRAGMA busy_timeout = 5000');
}
return _inbound;
@@ -41,8 +40,7 @@ export function getInboundDb(): Database {
/** Outbound DB — container owns this file (sole writer). */
export function getOutboundDb(): Database {
if (!_outbound) {
const dbPath = process.env.SESSION_OUTBOUND_DB_PATH || DEFAULT_OUTBOUND_PATH;
_outbound = new Database(dbPath);
_outbound = new Database(DEFAULT_OUTBOUND_PATH);
_outbound.exec('PRAGMA journal_mode = DELETE');
_outbound.exec('PRAGMA busy_timeout = 5000');
_outbound.exec('PRAGMA foreign_keys = ON');
@@ -122,7 +120,7 @@ export function clearContainerToolInFlight(): void {
* A file touch is cheaper and avoids cross-boundary DB write contention.
*/
export function touchHeartbeat(): void {
const p = process.env.SESSION_HEARTBEAT_PATH || _heartbeatPath;
const p = _heartbeatPath;
const now = new Date();
try {
fs.utimesSync(p, now, now);