switchroom 0.12.7 → 0.12.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11652,16 +11652,81 @@ bot.command('usage', async ctx => {
11652
11652
  await switchroomReply(ctx, formatQuotaBlock(result.data), { html: true })
11653
11653
  })
11654
11654
 
11655
+ // Two-button scope picker shown to admin agents (when hostd is
11656
+ // reachable) so the operator can run doctor for the WHOLE FLEET
11657
+ // (host-side via hostd — has the docker socket) or just THIS agent
11658
+ // (in-container, degraded). callback_data is tiny (`dr:fleet` /
11659
+ // `dr:self`) — well within Telegram's 64-byte limit.
11660
+ function buildDoctorScopeKeyboard(): InlineKeyboard {
11661
+ return new InlineKeyboard()
11662
+ .text('🩺 Whole fleet', 'dr:fleet')
11663
+ .text('🩺 This agent', 'dr:self')
11664
+ }
11665
+
11666
+ // Shared report prettifier: ANSI-strip + status-glyph swap + pre block.
11667
+ // Identical rendering for the in-container and the hostd fleet report.
11668
+ function formatDoctorReport(raw: string): string {
11669
+ const trimmed = stripAnsi(raw).trim()
11670
+ if (!trimmed) return 'doctor: no output'
11671
+ const pretty = trimmed
11672
+ .replace(/^( *)✓ /gm, '$1🟢 ')
11673
+ .replace(/^( *)✗ /gm, '$1🔴 ')
11674
+ .replace(/^( *)! /gm, '$1🟡 ')
11675
+ return preBlock(formatSwitchroomOutput(pretty))
11676
+ }
11677
+
11678
+ // In-container `switchroom doctor` — this agent's own (degraded: no
11679
+ // docker socket) view. The original /doctor behaviour, unchanged.
11680
+ async function renderSelfDoctor(ctx: Context): Promise<void> {
11681
+ let output: string
11682
+ try { output = switchroomExecCombined(['doctor'], 30000) }
11683
+ catch (err: unknown) { output = (err as any).stdout ?? (err as any).message ?? 'doctor failed' }
11684
+ await switchroomReply(ctx, formatDoctorReport(output), { html: true })
11685
+ }
11686
+
11687
+ // Whole-fleet `switchroom doctor` via hostd: it runs host-side where
11688
+ // the docker socket exists, so it sees every container + singleton
11689
+ // instead of the degraded in-container reading. Read-only verb; the
11690
+ // daemon independently enforces the admin gate (path-as-identity), so
11691
+ // this is the audited boundary even though the gateway only offers
11692
+ // the button to admin agents.
11693
+ async function renderFleetDoctor(ctx: Context): Promise<void> {
11694
+ const resp = await tryHostdDispatch(getMyAgentName(), {
11695
+ v: 1,
11696
+ op: 'doctor',
11697
+ request_id: hostdRequestId('gw-doctor'),
11698
+ })
11699
+ if (resp === 'not-configured') {
11700
+ await switchroomReply(ctx, '🩺 Whole-fleet doctor needs hostd, which isn’t configured here — showing this agent instead.', { html: true })
11701
+ await renderSelfDoctor(ctx)
11702
+ return
11703
+ }
11704
+ if (resp.result === 'denied') {
11705
+ await switchroomReply(ctx, `🩺 <b>Whole-fleet doctor denied by hostd:</b>\n${preBlock(formatSwitchroomOutput(resp.error ?? 'admin required'))}`, { html: true })
11706
+ return
11707
+ }
11708
+ // `completed` (including `switchroom doctor` exit 1 = "found
11709
+ // problems", which the handler classifies as completed) or `error`:
11710
+ // the report on stdout (or the error text) is exactly what the
11711
+ // operator wants surfaced.
11712
+ const body = resp.stdout_tail?.trim() || resp.error || '(no output from hostd doctor)'
11713
+ await switchroomReply(ctx, formatDoctorReport(body), { html: true })
11714
+ }
11715
+
11655
11716
  bot.command('doctor', async ctx => {
11656
11717
  if (!isAuthorizedSender(ctx)) return
11657
11718
  try {
11658
- let output: string
11659
- try { output = switchroomExecCombined(['doctor'], 30000) }
11660
- catch (err: unknown) { output = (err as any).stdout ?? (err as any).message ?? 'doctor failed' }
11661
- const trimmed = stripAnsi(output).trim()
11662
- if (!trimmed) { await switchroomReply(ctx, 'doctor: no output'); return }
11663
- const pretty = trimmed.replace(/^( *)✓ /gm, '$1🟢 ').replace(/^( *)✗ /gm, '$1🔴 ').replace(/^( *)! /gm, '$1🟡 ')
11664
- await switchroomReply(ctx, preBlock(formatSwitchroomOutput(pretty)), { html: true })
11719
+ // Admin agents with hostd reachable choose scope (one tap, no
11720
+ // approval card doctor is read-only). Everyone else keeps the
11721
+ // original zero-extra-tap in-container behaviour.
11722
+ if (AGENT_ADMIN && hostdWillBeUsed(getMyAgentName())) {
11723
+ await switchroomReply(ctx, '🩺 <b>Doctor</b> — which scope?', {
11724
+ html: true,
11725
+ reply_markup: buildDoctorScopeKeyboard(),
11726
+ })
11727
+ return
11728
+ }
11729
+ await renderSelfDoctor(ctx)
11665
11730
  } catch (err: unknown) {
11666
11731
  await switchroomReply(ctx, `<b>doctor failed:</b>\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
11667
11732
  }
@@ -11820,6 +11885,30 @@ bot.on('callback_query:data', async ctx => {
11820
11885
  return
11821
11886
  }
11822
11887
 
11888
+ // dr:<scope> — /doctor scope picker (only shown to admin agents
11889
+ // with hostd reachable). `dr:fleet` → host-side hostd `doctor`
11890
+ // (full fleet); `dr:self` → in-container doctor. Read-only, no
11891
+ // approval card. Same sender-auth as the /doctor command itself —
11892
+ // every callback family must re-auth (the absence of this check
11893
+ // was a past vulnerability class; see apv:/drvpick: notes above).
11894
+ if (data.startsWith('dr:')) {
11895
+ if (!isAuthorizedSender(ctx)) {
11896
+ await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
11897
+ return
11898
+ }
11899
+ const scope = data.slice(3)
11900
+ await ctx.answerCallbackQuery({
11901
+ text: scope === 'fleet' ? '🩺 Running fleet doctor…' : '🩺 Running doctor…',
11902
+ }).catch(() => {})
11903
+ try {
11904
+ if (scope === 'fleet') await renderFleetDoctor(ctx)
11905
+ else await renderSelfDoctor(ctx)
11906
+ } catch (err: unknown) {
11907
+ await switchroomReply(ctx, `<b>doctor failed:</b>\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
11908
+ }
11909
+ return
11910
+ }
11911
+
11823
11912
  // Issue #228: vault grant management callbacks.
11824
11913
  // vg:revoke:<id> — show confirmation card
11825
11914
  // vg:confirm:<id> — execute revoke