Files
nanoclaw/src/backfill-container-configs.ts
gavrielc aebcffe180 feat: per-group CLI scope (disabled/group/global)
Add cli_scope column to container_configs with three levels:
- disabled: agent never learns about ncl (instructions excluded from
  CLAUDE.md) and host dispatch rejects any cli_request
- group (default): agent can only access groups, sessions, destinations,
  and members resources, scoped to its own agent group with auto-filled
  --id/--agent_group_id/--group args. Help output reflects the scope.
- global: unrestricted access (current behavior)

Enforcement is host-side only — no image rebuild or env var needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:02:31 +03:00

79 lines
2.6 KiB
TypeScript

/**
* One-time backfill: seed `container_configs` rows from existing
* `groups/<folder>/container.json` files and `agent_groups.agent_provider`.
*
* Runs after migrations, before channel adapters start. Idempotent — skips
* groups that already have a config row.
*/
import fs from 'fs';
import path from 'path';
import { GROUPS_DIR } from './config.js';
import type { McpServerConfig, AdditionalMountConfig } from './container-config.js';
import { getAllAgentGroups } from './db/agent-groups.js';
import { getContainerConfig, createContainerConfig } from './db/container-configs.js';
import { log } from './log.js';
import type { ContainerConfigRow } from './types.js';
interface LegacyContainerJson {
mcpServers?: Record<string, McpServerConfig>;
packages?: { apt?: string[]; npm?: string[] };
imageTag?: string;
additionalMounts?: AdditionalMountConfig[];
skills?: string[] | 'all';
provider?: string;
assistantName?: string;
maxMessagesPerPrompt?: number;
}
export function backfillContainerConfigs(): void {
const groups = getAllAgentGroups();
let backfilled = 0;
for (const group of groups) {
// Skip if already has a config row
if (getContainerConfig(group.id)) continue;
// Read legacy container.json from disk
const filePath = path.join(GROUPS_DIR, group.folder, 'container.json');
let legacy: LegacyContainerJson = {};
if (fs.existsSync(filePath)) {
try {
legacy = JSON.parse(fs.readFileSync(filePath, 'utf8')) as LegacyContainerJson;
} catch (err) {
log.warn('Backfill: failed to parse container.json, using defaults', {
folder: group.folder,
err: String(err),
});
}
}
// DB agent_provider wins over file provider (matches old cascade)
const provider = group.agent_provider || legacy.provider || null;
const row: ContainerConfigRow = {
agent_group_id: group.id,
provider,
model: null,
effort: null,
image_tag: legacy.imageTag ?? null,
assistant_name: legacy.assistantName ?? null,
max_messages_per_prompt: legacy.maxMessagesPerPrompt ?? null,
skills: JSON.stringify(legacy.skills ?? 'all'),
mcp_servers: JSON.stringify(legacy.mcpServers ?? {}),
packages_apt: JSON.stringify(legacy.packages?.apt ?? []),
packages_npm: JSON.stringify(legacy.packages?.npm ?? []),
additional_mounts: JSON.stringify(legacy.additionalMounts ?? []),
cli_scope: 'group',
updated_at: new Date().toISOString(),
};
createContainerConfig(row);
backfilled++;
}
if (backfilled > 0) {
log.info('Backfilled container_configs from disk', { count: backfilled });
}
}