refactor(self-mod): drop request_rebuild — approvals now bundle rebuild+restart

install_packages and add_mcp_server already did the right thing on approve
(install auto-rebuilt+killed, add_mcp_server just killed), so request_rebuild
was redundant plumbing agents sometimes called after an install — wasting an
admin approval round-trip. Delete it end-to-end:

- container/agent-runner/src/mcp-tools/self-mod.ts: remove requestRebuild
  tool + registration; update install_packages description.
- src/modules/self-mod/{request,apply,index}.ts: drop handleRequestRebuild
  + applyRequestRebuild + registrations; rewrite the rebuild-failed notify
  to point admins at retrying install_packages instead.
- src/modules/{approvals,self-mod}/{agent,project}.md and skill/self-
  customize/SKILL.md: scrub agent-facing references; clarify that
  add_mcp_server needs no rebuild (bun runs TS directly).
- docs/{module-contract,architecture-diagram,checklist,db-central,shared-
  source,v1-vs-v2/*}.md, CLAUDE.md, pending-approvals migration comment,
  approvals/index.ts docstring, REFACTOR.md: trailing references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-22 17:28:36 +03:00
parent e64bdb3016
commit 3b8240a91b
20 changed files with 97 additions and 151 deletions

View File

@@ -3,8 +3,8 @@ import type { Migration } from './index.js';
/**
* `pending_approvals` table — host-side records for any approval-requiring
* request. Used by:
* - install_packages / request_rebuild / add_mcp_server (session-bound,
* `session_id` set, status stays at default 'pending' until handled)
* - install_packages / add_mcp_server (session-bound, `session_id` set,
* status stays at default 'pending' until handled)
* - OneCLI credential approvals from the SDK `configureManualApproval`
* callback (session_id may be null, action='onecli_credential').
*

View File

@@ -16,7 +16,7 @@ install_packages({
- Max 20 packages per request.
- Names must match strict regex (blocks shell injection via `vim; curl evil.com`).
- After approval: rebuild runs automatically. You do NOT need to call `request_rebuild` separately.
- On approval, the image rebuild and container restart happen automatically — there is no separate rebuild step for you to trigger.
### add_mcp_server
@@ -32,15 +32,7 @@ add_mcp_server({
```
- Does NOT install packages. Use `install_packages` first if the command isn't already available.
- On approval, container is killed so the next message wakes it with the new server wired up.
### request_rebuild
Rebuild your container image. Only useful if you've already landed `install_packages` approvals whose rebuild step failed, or if you're recovering from a bad config edit.
```
request_rebuild({ reason: "previous install_packages rebuild failed" })
```
- On approval, the container is killed and the next message wakes it with the new server wired up. No image rebuild — bun runs TS directly.
### How approval works

View File

@@ -12,9 +12,9 @@
* once the delivery adapter is set.
* - A shutdown callback that stops the OneCLI handler cleanly.
*
* Self-mod flows (install_packages, request_rebuild, add_mcp_server) moved
* out to `src/modules/self-mod/` in PR #7 — they now register delivery
* actions + approval handlers via this module's public API.
* Self-mod flows (install_packages, add_mcp_server) moved out to
* `src/modules/self-mod/` in PR #7 — they now register delivery actions
* + approval handlers via this module's public API.
*/
import { onDeliveryAdapterReady } from '../../delivery.js';
import { registerResponseHandler, onShutdown } from '../../response-registry.js';

View File

@@ -4,13 +4,13 @@ Admin-gated approval flow for agent self-modification and OneCLI credential acce
### Two flows
**Agent-initiated (DB-backed, fire-and-forget).** The container writes a `system`-kind outbound row with one of three actions — `install_packages`, `request_rebuild`, `add_mcp_server`. The module's delivery-action handlers validate, route to the right approver's DM, and persist a `pending_approvals` row. When the admin clicks a button, the registered response handler applies the change (config update → image rebuild → container kill) and notifies the agent via system chat.
**Agent-initiated (DB-backed, fire-and-forget).** The container writes a `system`-kind outbound row with one of two actions — `install_packages`, `add_mcp_server`. The module's delivery-action handlers validate, route to the right approver's DM, and persist a `pending_approvals` row. When the admin clicks a button, the registered response handler applies the change (config update → image rebuild if needed → container kill) and notifies the agent via system chat.
**OneCLI credential (long-poll).** The OneCLI gateway holds an HTTP connection open when it needs credential approval. `onecli-approvals.ts` delivers a card, persists a `pending_approvals` row (action = `onecli_credential`), and waits on an in-memory Promise that resolves on click or expiry timer. Survives host restart: the startup sweep edits stale cards to "Expired (host restarted)" and drops the rows.
### Wiring
- **Delivery actions:** `install_packages`, `request_rebuild`, `add_mcp_server` via `registerDeliveryAction`.
- **Delivery actions:** `install_packages`, `add_mcp_server` via `registerDeliveryAction`.
- **Response handler:** single handler claims both agent-initiated and OneCLI approvals. OneCLI is tried first (in-memory Promise); falls through to `pending_approvals` lookup.
- **Adapter-ready hook (`onDeliveryAdapterReady`):** starts the OneCLI manual-approval handler once the delivery adapter is set.
- **Shutdown hook (`onShutdown`):** stops the OneCLI handler.

View File

@@ -1,29 +1,28 @@
# Self-modification
You can install additional OS or npm packages, rebuild your container image,
or add new MCP servers — but only with admin approval.
You can install additional OS or npm packages or add new MCP servers — but
only with admin approval.
## Tools
- `install_packages({ apt?: string[], npm?: string[], reason?: string })`
adds the listed packages to your container config and rebuilds the image
after admin approval. Package names are validated strictly (`[a-z0-9._+-]`
for apt, standard npm naming with optional scope). Max 20 packages per
request.
- `request_rebuild({ reason?: string })` — rebuilds your container image
without config changes. Useful if the image has drifted from config.
adds the listed packages to your container config, rebuilds the image,
and restarts your container, all in a single admin approval step.
Package names are validated strictly (`[a-z0-9._+-]` for apt, standard
npm naming with optional scope). Max 20 packages per request.
- `add_mcp_server({ name, command, args?, env? })` — adds a new MCP server
to your container config. The container restarts on next message so the
new server is available.
to your container config and restarts the container so the new server
is wired up on the next message. No image rebuild is required (bun runs
TS directly).
## Flow
You call one of these tools → the host asks an admin via DM → admin approves
or rejects. On approve, the config is applied and the container is killed;
the host respawns it on the next message. You'll get a system chat message
confirming the outcome (either "Packages installed..." or a failure reason).
or rejects. On approve, the config is applied, the image is rebuilt if
needed, and the container is killed; the host respawns it on the next
message. You'll get a system chat message confirming the outcome (either
"Packages installed..." or a failure reason).
On reject you'll see "Your X request was rejected by admin."

View File

@@ -5,6 +5,11 @@
* pending_approvals row whose action matches. Each handler mutates the
* container config, rebuilds/kills the container as needed, and lets the
* host sweep respawn it on the new image on the next message.
*
* install_packages: rebuild image + kill container (apt/npm global installs
* must be baked into the image layer).
* add_mcp_server: kill container only — bun runs TS directly, so a pure
* MCP wiring change needs nothing more than a process restart.
*/
import { updateContainerConfig } from '../../container-config.js';
import { buildAgentGroupImage, killContainer } from '../../container-runner.js';
@@ -54,24 +59,12 @@ export const applyInstallPackages: ApprovalHandler = async ({ session, payload,
log.info('Container rebuild completed (bundled with install)', { agentGroupId: session.agent_group_id });
} catch (e) {
notify(
`Packages added to config (${pkgs}) but rebuild failed: ${e instanceof Error ? e.message : String(e)}. Call request_rebuild to retry.`,
`Packages added to config (${pkgs}) but rebuild failed: ${e instanceof Error ? e.message : String(e)}. Tell the user — an admin will need to retry the install_packages request or inspect the build logs.`,
);
log.error('Bundled rebuild failed after install approval', { agentGroupId: session.agent_group_id, err: e });
}
};
export const applyRequestRebuild: ApprovalHandler = async ({ session, userId, notify }) => {
try {
await buildAgentGroupImage(session.agent_group_id);
killContainer(session.id, 'rebuild applied');
notify('Container image rebuilt. Your container will restart with the new image on the next message.');
log.info('Container rebuild approved and completed', { agentGroupId: session.agent_group_id, userId });
} catch (e) {
notify(`Rebuild failed: ${e instanceof Error ? e.message : String(e)}`);
log.error('Container rebuild failed', { agentGroupId: session.agent_group_id, err: e });
}
};
export const applyAddMcpServer: ApprovalHandler = async ({ session, payload, userId, notify }) => {
const agentGroup = getAgentGroup(session.agent_group_id);
if (!agentGroup) {

View File

@@ -3,26 +3,28 @@
*
* Optional tier. Depends on the approvals default module for the request/
* handler plumbing. On install the module registers:
* - Three delivery actions (install_packages, request_rebuild, add_mcp_server)
* that validate input and queue an approval via requestApproval().
* - Three matching approval handlers that run on approve: mutate the
* container config, rebuild the image, kill the container so the next
* wake picks up the change.
* - Two delivery actions (install_packages, add_mcp_server) that validate
* input and queue an approval via requestApproval().
* - Two matching approval handlers that run on approve and perform the
* complete follow-up:
* install_packages → update container.json, rebuild image, kill
* container (next wake respawns on the new image), schedule a
* verify-and-report follow-up prompt.
* add_mcp_server → update container.json, kill container. No image
* rebuild — bun runs TS directly, so the new MCP server is wired
* by the next container start.
*
* Without this module: the three MCP tools in the container still write
* outbound system messages with these actions, but delivery logs
* "Unknown system action" and drops them. Admin never sees a card; nothing
* changes.
* Without this module: the MCP tools in the container still write outbound
* system messages with these actions, but delivery logs "Unknown system
* action" and drops them. Admin never sees a card; nothing changes.
*/
import { registerDeliveryAction } from '../../delivery.js';
import { registerApprovalHandler } from '../approvals/index.js';
import { applyAddMcpServer, applyInstallPackages, applyRequestRebuild } from './apply.js';
import { handleAddMcpServer, handleInstallPackages, handleRequestRebuild } from './request.js';
import { applyAddMcpServer, applyInstallPackages } from './apply.js';
import { handleAddMcpServer, handleInstallPackages } from './request.js';
registerDeliveryAction('install_packages', handleInstallPackages);
registerDeliveryAction('request_rebuild', handleRequestRebuild);
registerDeliveryAction('add_mcp_server', handleAddMcpServer);
registerApprovalHandler('install_packages', applyInstallPackages);
registerApprovalHandler('request_rebuild', applyRequestRebuild);
registerApprovalHandler('add_mcp_server', applyAddMcpServer);

View File

@@ -1,20 +1,25 @@
# Self-mod module
Optional-tier module that gives agents admin-gated self-modification:
installing OS/npm packages, rebuilding the container image, and registering
new MCP servers. All three paths go through the approvals module's request
primitive — no unapproved changes ever land.
installing OS/npm packages and registering new MCP servers. Both paths go
through the approvals module's request primitive — no unapproved changes
ever land. The rebuild+restart (or restart-only) follow-up is bundled into
the approval handler itself — there is no separate "request rebuild" step.
## What this module adds
- Three delivery actions (`install_packages`, `request_rebuild`, `add_mcp_server`)
that the container's self-mod MCP tools write into outbound.db. On the host,
each handler validates input and queues an approval via
- Two delivery actions (`install_packages`, `add_mcp_server`) that the
container's self-mod MCP tools write into outbound.db. On the host, each
handler validates input and queues an approval via
`approvals.requestApproval()`.
- Three matching approval handlers that run on approve: mutate the container
config via `updateContainerConfig`, rebuild the image via
`buildAgentGroupImage`, and kill the container so the host sweep respawns
it on the new image.
- Two matching approval handlers that run on approve:
- `install_packages`update `container.json`, rebuild the image via
`buildAgentGroupImage`, and kill the container so the host sweep
respawns it on the new image. Also schedules a verify-and-report
follow-up prompt ~5 s after kill.
- `add_mcp_server` → update `container.json` and kill the container.
No image rebuild — bun runs TS directly, so the new MCP wiring is
picked up on the next container start.
## Dependency

View File

@@ -1,10 +1,12 @@
/**
* 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.
* Two actions the container can write into messages_out (via the self-mod
* MCP tools): install_packages, add_mcp_server. Each one validates input
* and queues an approval request. The admin's approval triggers the
* matching approval handler in ./apply.ts, which also performs the
* required follow-up (rebuild+restart for install_packages, restart-only
* for add_mcp_server).
*
* Host-side sanitization for install_packages is defense-in-depth — the MCP
* tool validates first. Both layers matter: the DB row carries the payload
@@ -61,23 +63,6 @@ export async function handleInstallPackages(content: Record<string, unknown>, se
});
}
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) {