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.
- package/dist/agent-scheduler/index.js +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +539 -427
- package/dist/host-control/main.js +123 -100
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +96 -83
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +263 -204
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +96 -7
|
@@ -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
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
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
|