Promotes approvals to the default tier with a public API (requestApproval + registerApprovalHandler) that other modules consume. Self-modification (install_packages / request_rebuild / add_mcp_server) moves into a new optional module that registers delivery actions + matching approval handlers via the new API. ## Approvals (default tier) - Adds `src/modules/approvals/primitive.ts` exporting `requestApproval`, `registerApprovalHandler`, `notifyAgent`. Absorbs `pickApprover` / `pickApprovalDelivery` / `channelTypeOf` from the deleted `src/access.ts`. - Rewrites `response-handler.ts` to dispatch to registered approval handlers on approve (action-keyed Map). Reject path is centralized. - Drops the three self-mod-specific delivery-action registrations from `approvals/index.ts`; they belong to self-mod now. - `onecli-approvals.ts` now imports picks from the primitive instead of `src/access.ts`. ## Self-mod (optional tier) - New `src/modules/self-mod/` with request handlers (validate input + call requestApproval) and apply handlers (orchestration on approve). - `apply.ts` owns updateContainerConfig + buildAgentGroupImage + killContainer calls. Self-mod depends on approvals (via registerApprovalHandler + requestApproval + notifyAgent) and on core (container-runner, container-config). - Registers 3 delivery actions + 3 approval handlers at import time. ## Other changes - `src/access.ts` and `src/access.test.ts` deleted. Tests split across `src/modules/approvals/picks.test.ts` (approver selection) and `src/modules/permissions/permissions.test.ts` (access + roles + DM). - `src/modules/index.ts` barrel: approvals loads before self-mod so registerApprovalHandler is bound when self-mod registers at import time. ## Validation - `pnpm run build` clean - `pnpm test` — 137 host tests pass - `bun test` in container/agent-runner — 17 tests pass - Service starts; boot log shows `OneCLI approval handler started`, `NanoClaw running`; clean SIGTERM shutdown Resolves the transitional tier violation flagged in PR #5 where core imported from the permissions optional module via `src/access.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
/**
|
|
* Delivery-action handlers for agent-initiated self-modification requests.
|
|
*
|
|
* Three actions the container can write into messages_out (via the self-mod
|
|
* MCP tools): install_packages, request_rebuild, add_mcp_server. Each one
|
|
* validates input and queues an approval request. The admin's approval
|
|
* triggers the matching approval handler in ./apply.ts.
|
|
*
|
|
* Host-side sanitization for install_packages is defense-in-depth — the MCP
|
|
* tool validates first. Both layers matter: the DB row carries the payload
|
|
* verbatim through to shell exec on apply.
|
|
*/
|
|
import { getAgentGroup } from '../../db/agent-groups.js';
|
|
import { log } from '../../log.js';
|
|
import type { Session } from '../../types.js';
|
|
import { notifyAgent, requestApproval } from '../approvals/index.js';
|
|
|
|
export async function handleInstallPackages(content: Record<string, unknown>, session: Session): Promise<void> {
|
|
const agentGroup = getAgentGroup(session.agent_group_id);
|
|
if (!agentGroup) {
|
|
notifyAgent(session, 'install_packages failed: agent group not found.');
|
|
return;
|
|
}
|
|
|
|
const apt = (content.apt as string[]) || [];
|
|
const npm = (content.npm as string[]) || [];
|
|
const reason = (content.reason as string) || '';
|
|
|
|
const APT_RE = /^[a-z0-9][a-z0-9._+-]*$/;
|
|
const NPM_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
const MAX_PACKAGES = 20;
|
|
if (apt.length + npm.length === 0) {
|
|
notifyAgent(session, 'install_packages failed: at least one apt or npm package is required.');
|
|
return;
|
|
}
|
|
if (apt.length + npm.length > MAX_PACKAGES) {
|
|
notifyAgent(session, `install_packages failed: max ${MAX_PACKAGES} packages per request.`);
|
|
return;
|
|
}
|
|
const invalidApt = apt.find((p) => !APT_RE.test(p));
|
|
if (invalidApt) {
|
|
notifyAgent(session, `install_packages failed: invalid apt package name "${invalidApt}".`);
|
|
log.warn('install_packages: invalid apt package rejected', { pkg: invalidApt });
|
|
return;
|
|
}
|
|
const invalidNpm = npm.find((p) => !NPM_RE.test(p));
|
|
if (invalidNpm) {
|
|
notifyAgent(session, `install_packages failed: invalid npm package name "${invalidNpm}".`);
|
|
log.warn('install_packages: invalid npm package rejected', { pkg: invalidNpm });
|
|
return;
|
|
}
|
|
|
|
const packageList = [...apt.map((p) => `apt: ${p}`), ...npm.map((p) => `npm: ${p}`)].join(', ');
|
|
await requestApproval({
|
|
session,
|
|
agentName: agentGroup.name,
|
|
action: 'install_packages',
|
|
payload: { apt, npm, reason },
|
|
title: 'Install Packages Request',
|
|
question: `Agent "${agentGroup.name}" is attempting to install a package + rebuild container:\n${packageList}${reason ? `\nReason: ${reason}` : ''}`,
|
|
});
|
|
}
|
|
|
|
export async function handleRequestRebuild(content: Record<string, unknown>, session: Session): Promise<void> {
|
|
const agentGroup = getAgentGroup(session.agent_group_id);
|
|
if (!agentGroup) {
|
|
notifyAgent(session, 'request_rebuild failed: agent group not found.');
|
|
return;
|
|
}
|
|
const reason = (content.reason as string) || '';
|
|
await requestApproval({
|
|
session,
|
|
agentName: agentGroup.name,
|
|
action: 'request_rebuild',
|
|
payload: { reason },
|
|
title: 'Rebuild Request',
|
|
question: `Agent "${agentGroup.name}" is attempting to rebuild container.${reason ? `\nReason: ${reason}` : ''}`,
|
|
});
|
|
}
|
|
|
|
export async function handleAddMcpServer(content: Record<string, unknown>, session: Session): Promise<void> {
|
|
const agentGroup = getAgentGroup(session.agent_group_id);
|
|
if (!agentGroup) {
|
|
notifyAgent(session, 'add_mcp_server failed: agent group not found.');
|
|
return;
|
|
}
|
|
const serverName = content.name as string;
|
|
const command = content.command as string;
|
|
if (!serverName || !command) {
|
|
notifyAgent(session, 'add_mcp_server failed: name and command are required.');
|
|
return;
|
|
}
|
|
await requestApproval({
|
|
session,
|
|
agentName: agentGroup.name,
|
|
action: 'add_mcp_server',
|
|
payload: {
|
|
name: serverName,
|
|
command,
|
|
args: (content.args as string[]) || [],
|
|
env: (content.env as Record<string, string>) || {},
|
|
},
|
|
title: 'Add MCP Request',
|
|
question: `Agent "${agentGroup.name}" is attempting to add a new MCP server:\n${serverName} (${command})`,
|
|
});
|
|
}
|