From 1903fab5e8404e5d19ef63d42aadf45f1b059b33 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Tue, 14 Apr 2026 12:53:46 +0000 Subject: [PATCH] feat(v2/approvals): bundle install_packages + rebuild into one approval Install approval now auto-rebuilds the image and kills the container, replacing the prior two-card flow where the agent had to call request_rebuild separately after install_packages was approved. Queues a processAfter=+5s synthetic prompt so the respawned container verifies the new packages and reports back to the user. Adds two v2-checklist gaps found along the way: - /remote-control and /remote-control-end are v1 host-level commands not ported to v2 - messaging_groups.admin_user_id is hardcoded null at registration Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/v2-checklist.md | 2 ++ src/delivery.ts | 2 +- src/index.ts | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/v2-checklist.md b/docs/v2-checklist.md index 561b8103f..a812afdad 100644 --- a/docs/v2-checklist.md +++ b/docs/v2-checklist.md @@ -154,6 +154,8 @@ Status: [x] done, [~] partial, [ ] not started - [x] Admin user ID per group - [x] Admin-only command filtering in container - [ ] Admin model refactor — instance-level default admin (user + messaging app) for all approval routing, overridable per agent group; deliver approval cards to admin's DM when the platform supports it +- [ ] `/remote-control` and `/remote-control-end` are v1 host-level commands not ported to v2 — listed in `ADMIN_COMMANDS` (formatter.ts:13) but unhandled in poll-loop, so they fall through to the Claude SDK which replies "Unknown skill". Found when `/remote-control` failed in a Telegram DM after the admin check passed. +- [ ] `messaging_groups.admin_user_id` is hardcoded `null` at registration (`setup/register.ts:175`), so privileged commands like `/remote-control` always deny access until the column is manually backfilled in SQLite. Found when `/remote-control` failed in a freshly-registered Telegram DM. - [ ] Self-approval UX: when the user requesting a sensitive action IS the recorded admin/owner of the agent group, the agent still posts an Approve button back to that same person and narrates "waiting on admin approval" — confusing, redundant, and the "waiting" line stays visible even after the user immediately clicks approve. - [ ] Replace the `is_admin`/main vs. non-main distinction on agent groups with an explicit owner/admin model — every agent group has a recorded owner (the platform user who created or installed it) and an admin (who receives approvals and can change wiring); the two default to the same identity but can diverge (e.g. handoff). Drops the "main group = admin group" coupling. Downstream consequence: non-main group registration on Telegram must use the same pairing-code flow as main (`setup --step pair-telegram` with a `wire-to:` or `new-agent:` intent), since there's no longer a privileged "main" chat whose identity is trusted transitively — every group binds its own admin at registration time. - [x] Approval flow (sensitive action -> card to admin -> approve/reject -> execute) — `pending_approvals` table, `requestApproval()` helper, reuses interactive card infra diff --git a/src/delivery.ts b/src/delivery.ts index a8466b9aa..fdfc83730 100644 --- a/src/delivery.ts +++ b/src/delivery.ts @@ -633,7 +633,7 @@ async function handleSystemAction( agentGroup.name, 'install_packages', { apt, npm, reason }, - `Agent "${agentGroup.name}" requests package installation:\n${packageList}${reason ? `\nReason: ${reason}` : ''}`, + `Agent "${agentGroup.name}" requests package install + container rebuild:\n${packageList}${reason ? `\nReason: ${reason}` : ''}`, ); break; } diff --git a/src/index.ts b/src/index.ts index c3a478d93..77f4cb609 100644 --- a/src/index.ts +++ b/src/index.ts @@ -281,8 +281,31 @@ async function handleApprovalResponse( updateAgentGroup(session.agent_group_id, { container_config: JSON.stringify(containerConfig) }); const pkgs = [...(payload.apt || []), ...(payload.npm || [])].join(', '); - notify(`Packages approved (${pkgs}). Call request_rebuild to apply them.`); log.info('Package install approved', { approvalId: approval.approval_id, userId }); + try { + await buildAgentGroupImage(session.agent_group_id); + killContainer(session.id, 'rebuild applied'); + // Schedule a follow-up prompt a few seconds after kill so the host sweep + // respawns the container on the new image and the agent verifies + reports. + writeSessionMessage(session.agent_group_id, session.id, { + id: `appr-note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + kind: 'chat', + timestamp: new Date().toISOString(), + platformId: session.agent_group_id, + channelType: 'agent', + threadId: null, + content: JSON.stringify({ + text: `Packages installed (${pkgs}) and container rebuilt. Verify the new packages are available (e.g. run them or check versions) and report the result to the user.`, + sender: 'system', + senderId: 'system', + }), + processAfter: new Date(Date.now() + 5000).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ''), + }); + log.info('Container rebuild completed (bundled with install)', { approvalId: approval.approval_id }); + } catch (e) { + notify(`Packages added to config (${pkgs}) but rebuild failed: ${e instanceof Error ? e.message : String(e)}. Call request_rebuild to retry.`); + log.error('Bundled rebuild failed after install approval', { approvalId: approval.approval_id, err: e }); + } } else if (approval.action === 'request_rebuild') { try { await buildAgentGroupImage(session.agent_group_id);