switchroom 0.13.53 → 0.13.55
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 +53 -1
- package/dist/auth-broker/index.js +53 -1
- package/dist/cli/ms-365-write-pretool.mjs +259 -0
- package/dist/cli/notion-write-pretool.mjs +13388 -0
- package/dist/cli/switchroom.js +1601 -380
- package/dist/host-control/main.js +53 -1
- package/dist/vault/approvals/kernel-server.js +54 -2
- package/dist/vault/broker/server.js +54 -2
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +17 -0
- package/profiles/_shared/telegram-style.md.hbs +2 -0
- package/skills/notion/SKILL.md +144 -0
- package/telegram-plugin/dist/gateway/gateway.js +406 -43
- package/telegram-plugin/gateway/gateway.ts +227 -17
- package/telegram-plugin/gateway/ipc-protocol.ts +37 -0
- package/telegram-plugin/gateway/ipc-server.ts +59 -0
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +314 -0
- package/telegram-plugin/gateway/ms365-write-approval.ts +335 -0
- package/telegram-plugin/tests/ipc-validator.test.ts +61 -0
- package/telegram-plugin/tests/slash-command-smart-split.test.ts +115 -0
- package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +35 -0
- package/vendor/hindsight-memory/scripts/recall.py +164 -4
- package/vendor/hindsight-memory/scripts/retain.py +52 -0
- package/vendor/hindsight-memory/scripts/tests/test_gateway_ipc.py +42 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_topic_filter.py +139 -0
|
@@ -265,6 +265,7 @@ import { shouldSweepChatAtBoot } from './boot-sweep-filter.js'
|
|
|
265
265
|
|
|
266
266
|
import { createIpcServer, type IpcClient, type IpcServer } from './ipc-server.js'
|
|
267
267
|
import { handleRequestDriveApproval } from './drive-write-approval.js'
|
|
268
|
+
import { handleRequestMs365Approval } from './ms365-write-approval.js'
|
|
268
269
|
import { buildDiffPreviewCard } from './diff-preview-card.js'
|
|
269
270
|
import { createPendingInboundBuffer, redeliverBufferedInbound, idleDrainTick } from './pending-inbound-buffer.js'
|
|
270
271
|
import { createInboundSpool } from './inbound-spool.js'
|
|
@@ -2999,8 +3000,18 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
2999
3000
|
return
|
|
3000
3001
|
}
|
|
3001
3002
|
|
|
3003
|
+
// PR4b emitter sweep: supergroup-mode agents route operator-event
|
|
3004
|
+
// cards (boot crash, restart-watchdog, model-unavailable, etc.) into
|
|
3005
|
+
// the alerts/admin alias topic instead of chat-root. The
|
|
3006
|
+
// 'compact-watchdog' kind covers all system-initiated notifications
|
|
3007
|
+
// — alerts alias → default_topic_id fallback (see router contract).
|
|
3008
|
+
// Fleet-shared / DM agents see `undefined` → no `message_thread_id`
|
|
3009
|
+
// is added to the broadcast opts → behavior unchanged.
|
|
3010
|
+
const opEventTopic = resolveAgentOutboundTopic({ kind: 'compact-watchdog' })
|
|
3011
|
+
|
|
3002
3012
|
process.stderr.write(
|
|
3003
|
-
`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)
|
|
3013
|
+
`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` +
|
|
3014
|
+
(opEventTopic != null ? ` topic=${opEventTopic}` : '') + '\n',
|
|
3004
3015
|
)
|
|
3005
3016
|
for (const chat_id of access.allowFrom) {
|
|
3006
3017
|
// grammy's Other<...> opts type is generated and stricter than our
|
|
@@ -3008,8 +3019,12 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
3008
3019
|
const opts = {
|
|
3009
3020
|
parse_mode: 'HTML' as const,
|
|
3010
3021
|
...(renderedKeyboard ? { reply_markup: renderedKeyboard } : {}),
|
|
3022
|
+
...(opEventTopic != null ? { message_thread_id: opEventTopic } : {}),
|
|
3011
3023
|
}
|
|
3012
|
-
//
|
|
3024
|
+
// Comment-only context for the reader; the lint marker on the
|
|
3025
|
+
// very next line is what unlocks the raw bot.api call.
|
|
3026
|
+
// Opts now includes message_thread_id when supergroup mode is on.
|
|
3027
|
+
// allow-raw-bot-api: operator-event broadcast loop; topic-aware opts
|
|
3013
3028
|
void bot.api.sendMessage(chat_id, renderedText, opts as never).catch(e => {
|
|
3014
3029
|
process.stderr.write(
|
|
3015
3030
|
`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}\n`,
|
|
@@ -4009,13 +4024,36 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
4009
4024
|
.row()
|
|
4010
4025
|
.text(`🔁 Always allow ${alwaysRule.label}`, `perm:always:${requestId}`)
|
|
4011
4026
|
}
|
|
4027
|
+
// PR4b emitter sweep — supergroup-mode permission card routing.
|
|
4028
|
+
// Per CPO #3 the design is "turn-initiated requests follow the
|
|
4029
|
+
// conversation topic; background requests go to admin alias."
|
|
4030
|
+
// Permission requests come from the bridge mid-tool-use, so they
|
|
4031
|
+
// are always turn-initiated in practice — the currently active
|
|
4032
|
+
// turn's sessionThreadId is the originating topic. Fall back to
|
|
4033
|
+
// admin alias when no active turn (cron / background path).
|
|
4034
|
+
// Fleet-shared / DM agents see `undefined` → no
|
|
4035
|
+
// `message_thread_id` is added → behavior unchanged.
|
|
4036
|
+
// currentTurn is the singleton "claude is currently on this turn"
|
|
4037
|
+
// pointer — per Framing 1 / PR3b scope-discovery, claude
|
|
4038
|
+
// serializes so there's exactly one (or zero) active turn at any
|
|
4039
|
+
// moment. When set, the permission request is in-flight for that
|
|
4040
|
+
// turn and follows the originating topic.
|
|
4041
|
+
const activeTurn = currentTurn
|
|
4042
|
+
const permTopic = resolveAgentOutboundTopic({
|
|
4043
|
+
kind: 'permission',
|
|
4044
|
+
turnInitiated: activeTurn != null,
|
|
4045
|
+
originThreadId: activeTurn?.sessionThreadId,
|
|
4046
|
+
})
|
|
4012
4047
|
for (const chat_id of access.allowFrom) {
|
|
4013
4048
|
// parse_mode=HTML pairs with formatPermissionCardBody (#1790)
|
|
4014
4049
|
// so the <b>/<i> tags render as formatting.
|
|
4015
|
-
//
|
|
4050
|
+
// PR4b emitter sweep — opts now optionally carries
|
|
4051
|
+
// message_thread_id when supergroup mode is on.
|
|
4052
|
+
// allow-raw-bot-api: permission-request keyboard fan-out; topic-aware opts
|
|
4016
4053
|
void bot.api.sendMessage(chat_id, text, {
|
|
4017
4054
|
parse_mode: 'HTML',
|
|
4018
4055
|
reply_markup: keyboard,
|
|
4056
|
+
...(permTopic != null ? { message_thread_id: permTopic } : {}),
|
|
4019
4057
|
}).catch(e => {
|
|
4020
4058
|
process.stderr.write(`telegram gateway: permission_request send to ${chat_id} failed: ${e}\n`)
|
|
4021
4059
|
})
|
|
@@ -4138,7 +4176,18 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
4138
4176
|
// routing surface (see how /folders posts) — this picks the
|
|
4139
4177
|
// DM path which is the common case; group-routing follow-up
|
|
4140
4178
|
// can extend this.
|
|
4141
|
-
|
|
4179
|
+
// PR4b emitter sweep — supergroup-mode adds an explicit
|
|
4180
|
+
// topic. Drive approval cards follow the originating turn
|
|
4181
|
+
// (operator-initiated tool call), admin alias fallback.
|
|
4182
|
+
const activeTurn = currentTurn
|
|
4183
|
+
const driveTopic = resolveAgentOutboundTopic({
|
|
4184
|
+
kind: 'hostd-approval',
|
|
4185
|
+
originThreadId: activeTurn?.sessionThreadId,
|
|
4186
|
+
})
|
|
4187
|
+
return {
|
|
4188
|
+
chatId: operator,
|
|
4189
|
+
...(driveTopic != null ? { threadId: driveTopic } : {}),
|
|
4190
|
+
}
|
|
4142
4191
|
},
|
|
4143
4192
|
registerApproval: async (args) => {
|
|
4144
4193
|
const r = await kernelApprovalRequest({
|
|
@@ -4186,6 +4235,76 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
4186
4235
|
})
|
|
4187
4236
|
},
|
|
4188
4237
|
|
|
4238
|
+
/**
|
|
4239
|
+
* RFC #1873 §8 — MS-365 write approval card. Mirrors
|
|
4240
|
+
* onRequestDriveApproval but with the weak-metadata v1 preview shape
|
|
4241
|
+
* (no diff-preview — just plain text card).
|
|
4242
|
+
*/
|
|
4243
|
+
async onRequestMs365Approval(client: IpcClient, msg) {
|
|
4244
|
+
await handleRequestMs365Approval(client, msg, {
|
|
4245
|
+
agentName: getMyAgentName(),
|
|
4246
|
+
loadAllowFrom: () => loadAccess().allowFrom,
|
|
4247
|
+
loadTargetChat: () => {
|
|
4248
|
+
const access = loadAccess()
|
|
4249
|
+
const operator = access.allowFrom[0]
|
|
4250
|
+
if (operator === undefined) return null
|
|
4251
|
+
// PR4b emitter sweep — MS365 write approval cards route into
|
|
4252
|
+
// the originating turn's topic in supergroup mode, admin
|
|
4253
|
+
// alias fallback for background cases. Same shape as hostd /
|
|
4254
|
+
// drive approvals below.
|
|
4255
|
+
const activeTurn = currentTurn
|
|
4256
|
+
const ms365Topic = resolveAgentOutboundTopic({
|
|
4257
|
+
kind: 'hostd-approval',
|
|
4258
|
+
originThreadId: activeTurn?.sessionThreadId,
|
|
4259
|
+
})
|
|
4260
|
+
return {
|
|
4261
|
+
chatId: operator,
|
|
4262
|
+
...(ms365Topic != null ? { threadId: ms365Topic } : {}),
|
|
4263
|
+
}
|
|
4264
|
+
},
|
|
4265
|
+
registerApproval: async (args) => {
|
|
4266
|
+
const r = await kernelApprovalRequest({
|
|
4267
|
+
agent_unit: args.agent_unit,
|
|
4268
|
+
scope: args.scope,
|
|
4269
|
+
action: args.action,
|
|
4270
|
+
approver_set: args.approver_set,
|
|
4271
|
+
why: args.why,
|
|
4272
|
+
ttl_ms: args.ttl_ms,
|
|
4273
|
+
})
|
|
4274
|
+
if (r === null || r.state === 'rate_limited') return null
|
|
4275
|
+
return {
|
|
4276
|
+
request_id: r.request_id,
|
|
4277
|
+
expires_at_ms: r.expires_at,
|
|
4278
|
+
}
|
|
4279
|
+
},
|
|
4280
|
+
postCard: async (args) => {
|
|
4281
|
+
try {
|
|
4282
|
+
const sent = await robustApiCall(
|
|
4283
|
+
() =>
|
|
4284
|
+
bot.api.sendMessage(args.chatId, args.text, {
|
|
4285
|
+
...(args.threadId !== undefined
|
|
4286
|
+
? { message_thread_id: args.threadId }
|
|
4287
|
+
: {}),
|
|
4288
|
+
reply_markup: args.replyMarkup as never,
|
|
4289
|
+
}),
|
|
4290
|
+
{
|
|
4291
|
+
chat_id: String(args.chatId),
|
|
4292
|
+
verb: 'ms365-approval-card',
|
|
4293
|
+
...(args.threadId !== undefined ? { threadId: args.threadId } : {}),
|
|
4294
|
+
},
|
|
4295
|
+
)
|
|
4296
|
+
return { messageId: (sent as { message_id: number }).message_id }
|
|
4297
|
+
} catch (err) {
|
|
4298
|
+
process.stderr.write(
|
|
4299
|
+
`telegram gateway: ms365-approval postCard failed: ${(err as Error).message}\n`,
|
|
4300
|
+
)
|
|
4301
|
+
return null
|
|
4302
|
+
}
|
|
4303
|
+
},
|
|
4304
|
+
log: (m) => process.stderr.write(`telegram gateway: ms365-approval — ${m}\n`),
|
|
4305
|
+
})
|
|
4306
|
+
},
|
|
4307
|
+
|
|
4189
4308
|
/**
|
|
4190
4309
|
* #1623 — hostd-initiated config-edit approval card. hostd posts a
|
|
4191
4310
|
* `request_config_approval` message; we render the card via
|
|
@@ -4206,7 +4325,21 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
4206
4325
|
const access = loadAccess()
|
|
4207
4326
|
const operator = access.allowFrom[0]
|
|
4208
4327
|
if (operator === undefined) return null
|
|
4209
|
-
|
|
4328
|
+
// PR4b emitter sweep — hostd config-approval cards are
|
|
4329
|
+
// operator-initiated (someone typed /update apply or tapped
|
|
4330
|
+
// a button). Follow the originating turn when there is one;
|
|
4331
|
+
// admin alias fallback in supergroup mode otherwise. Helper
|
|
4332
|
+
// returns undefined for non-supergroup agents → behavior
|
|
4333
|
+
// unchanged.
|
|
4334
|
+
const activeTurn = currentTurn
|
|
4335
|
+
const cfgTopic = resolveAgentOutboundTopic({
|
|
4336
|
+
kind: 'hostd-approval',
|
|
4337
|
+
originThreadId: activeTurn?.sessionThreadId,
|
|
4338
|
+
})
|
|
4339
|
+
return {
|
|
4340
|
+
chatId: operator,
|
|
4341
|
+
...(cfgTopic != null ? { threadId: cfgTopic } : {}),
|
|
4342
|
+
}
|
|
4210
4343
|
},
|
|
4211
4344
|
buildKeyboard: (requestId) =>
|
|
4212
4345
|
new InlineKeyboard()
|
|
@@ -9156,10 +9289,26 @@ type SwitchroomReplyMarkup =
|
|
|
9156
9289
|
async function switchroomReply(
|
|
9157
9290
|
ctx: Context,
|
|
9158
9291
|
text: string,
|
|
9159
|
-
options: {
|
|
9292
|
+
options: {
|
|
9293
|
+
html?: boolean
|
|
9294
|
+
reply_markup?: SwitchroomReplyMarkup
|
|
9295
|
+
// PR5 — supergroup-mode slash-command smart split (CPO #4).
|
|
9296
|
+
// 'query' (default semantics) follows the originating topic;
|
|
9297
|
+
// 'mutation' and 'heavy' route to admin alias in supergroup mode;
|
|
9298
|
+
// omitted/undefined preserves legacy in-place reply behavior.
|
|
9299
|
+
classification?: 'query' | 'mutation' | 'heavy'
|
|
9300
|
+
} = {},
|
|
9160
9301
|
): Promise<void> {
|
|
9161
9302
|
const chatId = String(ctx.chat!.id)
|
|
9162
|
-
const
|
|
9303
|
+
const baseThreadId = resolveThreadId(chatId, ctx.message?.message_thread_id)
|
|
9304
|
+
// PR5 — when caller flagged the reply as mutation/heavy AND we're
|
|
9305
|
+
// in supergroup mode, override the in-place thread with the
|
|
9306
|
+
// admin-alias topic. For 'query' (or non-supergroup), routedOpts
|
|
9307
|
+
// is `{}` and baseThreadId wins → behavior unchanged.
|
|
9308
|
+
const routedOpts = options.classification
|
|
9309
|
+
? slashCommandReplyOpts(ctx, options.classification)
|
|
9310
|
+
: {}
|
|
9311
|
+
const threadId = routedOpts.message_thread_id ?? baseThreadId
|
|
9163
9312
|
await ctx.reply(text, {
|
|
9164
9313
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
9165
9314
|
...(options.html ? { parse_mode: 'HTML' as const, link_preview_options: { is_disabled: true } } : {}),
|
|
@@ -9469,6 +9618,53 @@ function resolveSystemdRunPath(): string | null {
|
|
|
9469
9618
|
return _systemdRunPath
|
|
9470
9619
|
}
|
|
9471
9620
|
|
|
9621
|
+
/**
|
|
9622
|
+
* PR5 — supergroup-mode slash-command smart split (CPO #4 design).
|
|
9623
|
+
*
|
|
9624
|
+
* Classifies a slash-command response by event class and returns the
|
|
9625
|
+
* `message_thread_id` an outbound reply should target in supergroup
|
|
9626
|
+
* mode, or `undefined` when the agent isn't in supergroup-owned mode
|
|
9627
|
+
* (caller falls through to grammY's default `ctx.reply` which
|
|
9628
|
+
* routes to the originating topic).
|
|
9629
|
+
*
|
|
9630
|
+
* Three classes:
|
|
9631
|
+
* - `query` (`/help`, `/status`, etc.) — follows the originating
|
|
9632
|
+
* topic, identical to default `ctx.reply` behaviour.
|
|
9633
|
+
* - `mutation` (`/restart`, `/update apply`) — admin alias.
|
|
9634
|
+
* - `heavy` (`/logs`, `/audit`, `/upgradestatus`, `/memory <q>`)
|
|
9635
|
+
* — admin alias (operational separation per CPO #4).
|
|
9636
|
+
*
|
|
9637
|
+
* Callers spread the returned topic onto their send opts:
|
|
9638
|
+
*
|
|
9639
|
+
* await ctx.reply(text, {
|
|
9640
|
+
* ...opts,
|
|
9641
|
+
* ...slashCommandReplyOpts(ctx, 'heavy'),
|
|
9642
|
+
* })
|
|
9643
|
+
*
|
|
9644
|
+
* Returns `{}` when no override is needed (non-supergroup, or
|
|
9645
|
+
* helper resolved to the originating topic). Returns
|
|
9646
|
+
* `{ message_thread_id: N }` otherwise.
|
|
9647
|
+
*/
|
|
9648
|
+
function slashCommandReplyOpts(
|
|
9649
|
+
ctx: Context,
|
|
9650
|
+
classification: 'query' | 'mutation' | 'heavy',
|
|
9651
|
+
): { message_thread_id?: number } {
|
|
9652
|
+
const originThreadId = ctx.message?.message_thread_id
|
|
9653
|
+
const event =
|
|
9654
|
+
classification === 'query'
|
|
9655
|
+
? ({ kind: 'command-query', originThreadId } as const)
|
|
9656
|
+
: classification === 'mutation'
|
|
9657
|
+
? ({ kind: 'command-mutation' } as const)
|
|
9658
|
+
: ({ kind: 'command-heavy' } as const)
|
|
9659
|
+
const target = resolveAgentOutboundTopic(event)
|
|
9660
|
+
if (target == null) return {}
|
|
9661
|
+
// Avoid the spurious explicit thread_id when it would equal the
|
|
9662
|
+
// implicit-reply destination — keeps the wire identical to default
|
|
9663
|
+
// ctx.reply for the query class in supergroup mode.
|
|
9664
|
+
if (target === originThreadId) return {}
|
|
9665
|
+
return { message_thread_id: target }
|
|
9666
|
+
}
|
|
9667
|
+
|
|
9472
9668
|
/**
|
|
9473
9669
|
* Detect whether `docker` is callable from this process — required by
|
|
9474
9670
|
* `switchroom update`'s pull-images and recreate-containers steps.
|
|
@@ -9920,18 +10116,27 @@ async function dispatchShortVerbViaHostd(
|
|
|
9920
10116
|
)
|
|
9921
10117
|
}
|
|
9922
10118
|
|
|
9923
|
-
async function runSwitchroomCommand(
|
|
10119
|
+
async function runSwitchroomCommand(
|
|
10120
|
+
ctx: Context,
|
|
10121
|
+
args: string[],
|
|
10122
|
+
label: string,
|
|
10123
|
+
// PR5 — supergroup-mode classification threaded through to
|
|
10124
|
+
// switchroomReply. Default 'query' so existing callers (most
|
|
10125
|
+
// commands) preserve in-place reply behavior. /logs /audit
|
|
10126
|
+
// /upgradestatus /memory pass 'heavy' to route to admin alias.
|
|
10127
|
+
classification: 'query' | 'mutation' | 'heavy' = 'query',
|
|
10128
|
+
): Promise<void> {
|
|
9924
10129
|
try {
|
|
9925
10130
|
const output = stripAnsi(switchroomExec(args))
|
|
9926
10131
|
const formatted = formatSwitchroomOutput(output)
|
|
9927
|
-
if (formatted) { await switchroomReply(ctx, preBlock(formatted), { html: true }) }
|
|
9928
|
-
else { await switchroomReply(ctx, `${label}: done (no output)
|
|
10132
|
+
if (formatted) { await switchroomReply(ctx, preBlock(formatted), { html: true, classification }) }
|
|
10133
|
+
else { await switchroomReply(ctx, `${label}: done (no output)`, { classification }) }
|
|
9929
10134
|
} catch (err: unknown) {
|
|
9930
10135
|
const error = err as { status?: number; stderr?: string; message?: string }
|
|
9931
|
-
if (error.message?.includes('ENOENT')) { await switchroomReply(ctx, 'switchroom CLI not found.', { html: true }); return }
|
|
9932
|
-
if (error.message?.includes('ETIMEDOUT') || error.message?.includes('timed out')) { await switchroomReply(ctx, `${label}: timed out
|
|
10136
|
+
if (error.message?.includes('ENOENT')) { await switchroomReply(ctx, 'switchroom CLI not found.', { html: true, classification }); return }
|
|
10137
|
+
if (error.message?.includes('ETIMEDOUT') || error.message?.includes('timed out')) { await switchroomReply(ctx, `${label}: timed out`, { classification }); return }
|
|
9933
10138
|
const detail = stripAnsi(error.stderr?.trim() || error.message || 'unknown error')
|
|
9934
|
-
await switchroomReply(ctx, `<b>${escapeHtmlForTg(label)} failed:</b>\n${preBlock(formatSwitchroomOutput(detail))}`, { html: true })
|
|
10139
|
+
await switchroomReply(ctx, `<b>${escapeHtmlForTg(label)} failed:</b>\n${preBlock(formatSwitchroomOutput(detail))}`, { html: true, classification })
|
|
9935
10140
|
}
|
|
9936
10141
|
}
|
|
9937
10142
|
|
|
@@ -10896,7 +11101,9 @@ bot.command('update', async ctx => {
|
|
|
10896
11101
|
// /upgrade-status. The /upgrade alias just below redirects.)
|
|
10897
11102
|
bot.command('upgradestatus', async ctx => {
|
|
10898
11103
|
if (!isAuthorizedSender(ctx)) return
|
|
10899
|
-
|
|
11104
|
+
// PR5 — heavy-output: route to admin alias in supergroup mode
|
|
11105
|
+
// (CPO #4). Fleet-shared / DM agents fall through to in-place reply.
|
|
11106
|
+
await runSwitchroomCommand(ctx, ['update', '--status'], 'update --status', 'heavy')
|
|
10900
11107
|
})
|
|
10901
11108
|
// Alias with hyphen — Grammy doesn't allow hyphens in command names
|
|
10902
11109
|
// (Telegram's slash-command grammar excludes them) but operators are
|
|
@@ -10984,7 +11191,8 @@ bot.command('audit', async ctx => {
|
|
|
10984
11191
|
)
|
|
10985
11192
|
return
|
|
10986
11193
|
}
|
|
10987
|
-
|
|
11194
|
+
// PR5 — heavy-output → admin alias in supergroup mode (CPO #4).
|
|
11195
|
+
await runSwitchroomCommand(ctx, argv, `hostd audit${argv.length > 2 ? ' …' : ''}`, 'heavy')
|
|
10988
11196
|
})
|
|
10989
11197
|
|
|
10990
11198
|
// ─── /approve, /deny, /pending ────────────────────────────────────────────
|
|
@@ -13766,14 +13974,16 @@ bot.command('logs', async ctx => {
|
|
|
13766
13974
|
try { assertSafeAgentName(name) } catch { await switchroomReply(ctx, 'Invalid agent name.'); return }
|
|
13767
13975
|
const lines = linesArg ? parseInt(linesArg, 10) : 20
|
|
13768
13976
|
const lineCount = isNaN(lines) || lines < 1 ? 20 : Math.min(lines, 200)
|
|
13769
|
-
|
|
13977
|
+
// PR5 — heavy-output → admin alias in supergroup mode (CPO #4).
|
|
13978
|
+
await runSwitchroomCommand(ctx, ['agent', 'logs', name, '--lines', String(lineCount)], `logs ${name}`, 'heavy')
|
|
13770
13979
|
})
|
|
13771
13980
|
|
|
13772
13981
|
bot.command('memory', async ctx => {
|
|
13773
13982
|
if (!isAuthorizedSender(ctx)) return
|
|
13774
13983
|
const query = ctx.match?.trim()
|
|
13775
13984
|
if (!query) { await switchroomReply(ctx, 'Usage: /memory <search query>'); return }
|
|
13776
|
-
|
|
13985
|
+
// PR5 — heavy-output → admin alias in supergroup mode (CPO #4).
|
|
13986
|
+
await runSwitchroomCommand(ctx, ['memory', 'search', query], 'memory search', 'heavy')
|
|
13777
13987
|
})
|
|
13778
13988
|
|
|
13779
13989
|
bot.command('issues', async ctx => {
|
|
@@ -127,6 +127,7 @@ export type GatewayToClient =
|
|
|
127
127
|
| ToolCallResult
|
|
128
128
|
| ScheduleRestartResult
|
|
129
129
|
| DriveApprovalPostedEvent
|
|
130
|
+
| Ms365ApprovalPostedEvent
|
|
130
131
|
| ConfigApprovalResolvedEvent;
|
|
131
132
|
|
|
132
133
|
// === Bridge (Client) -> Gateway messages ===
|
|
@@ -306,6 +307,41 @@ export interface RequestDriveApprovalMessage {
|
|
|
306
307
|
ttlMs?: number;
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/**
|
|
311
|
+
* RFC #1873 §8 — Microsoft 365 write approval (PR 4).
|
|
312
|
+
*
|
|
313
|
+
* Sent by the `ms-365-write-pretool` PreToolUse hook when softeria
|
|
314
|
+
* tries a gated write tool (OneDrive upload, calendar/mail mutations).
|
|
315
|
+
* Same shape as `request_drive_approval` but carries the weak-metadata
|
|
316
|
+
* v1 preview shape (file path / id / size delta / deep link / agent
|
|
317
|
+
* rationale) rather than Google's full DiffPreviewInput. Structural-
|
|
318
|
+
* diff preview is RFC §8 v1.5.
|
|
319
|
+
*/
|
|
320
|
+
export interface RequestMs365ApprovalMessage {
|
|
321
|
+
type: "request_ms365_approval";
|
|
322
|
+
correlationId: string;
|
|
323
|
+
agentName: string;
|
|
324
|
+
/**
|
|
325
|
+
* Weak-metadata payload — see Ms365WritePreview in
|
|
326
|
+
* `telegram-plugin/gateway/ms365-write-approval.ts`. Opaque on the
|
|
327
|
+
* wire; gateway validates via the handler.
|
|
328
|
+
*/
|
|
329
|
+
preview: Record<string, unknown>;
|
|
330
|
+
ttlMs?: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Gateway → hook response after card is posted (or fails).
|
|
335
|
+
*/
|
|
336
|
+
export interface Ms365ApprovalPostedEvent {
|
|
337
|
+
type: "ms365_approval_posted";
|
|
338
|
+
correlationId: string;
|
|
339
|
+
ok: boolean;
|
|
340
|
+
requestId?: string;
|
|
341
|
+
expiresAtMs?: number;
|
|
342
|
+
reason?: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
309
345
|
/**
|
|
310
346
|
* hostd config-edit approval — sent by hostd to the caller agent's
|
|
311
347
|
* gateway to render an approval card with the full unified diff in
|
|
@@ -367,5 +403,6 @@ export type ClientToGateway =
|
|
|
367
403
|
| UpdatePlaceholderMessage
|
|
368
404
|
| InjectInboundMessage
|
|
369
405
|
| RequestDriveApprovalMessage
|
|
406
|
+
| RequestMs365ApprovalMessage
|
|
370
407
|
| RequestConfigApprovalMessage
|
|
371
408
|
| RequestConfigFinalizeMessage;
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RequestConfigApprovalMessage,
|
|
12
12
|
RequestConfigFinalizeMessage,
|
|
13
13
|
RequestDriveApprovalMessage,
|
|
14
|
+
RequestMs365ApprovalMessage,
|
|
14
15
|
ScheduleRestartMessage,
|
|
15
16
|
SessionEventForward,
|
|
16
17
|
ToolCallMessage,
|
|
@@ -55,6 +56,15 @@ export interface IpcServerOptions {
|
|
|
55
56
|
client: IpcClient,
|
|
56
57
|
msg: RequestDriveApprovalMessage,
|
|
57
58
|
) => Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* RFC #1873 §8 — Microsoft 365 write approval (PR 4). Same shape as
|
|
61
|
+
* onRequestDriveApproval but for softeria write tools. Optional;
|
|
62
|
+
* gateways without M365 integration ignore.
|
|
63
|
+
*/
|
|
64
|
+
onRequestMs365Approval?: (
|
|
65
|
+
client: IpcClient,
|
|
66
|
+
msg: RequestMs365ApprovalMessage,
|
|
67
|
+
) => Promise<void>;
|
|
58
68
|
/**
|
|
59
69
|
* #1623 — hostd-initiated config-edit approval card. Handler posts
|
|
60
70
|
* a Telegram card with [✅ Approve] [🚫 Deny] buttons, tracks the
|
|
@@ -273,6 +283,22 @@ export function validateClientMessage(msg: unknown): msg is ClientToGateway {
|
|
|
273
283
|
|| (m.ttlMs as number) < 0)) return false;
|
|
274
284
|
return true;
|
|
275
285
|
}
|
|
286
|
+
case "request_ms365_approval": {
|
|
287
|
+
// RFC #1873 §8 PR 4. Same wire-shape gate as Drive — gateway
|
|
288
|
+
// routes on the outer fields; the inner `preview` is opaque
|
|
289
|
+
// and re-validated by `validateMs365Preview()` downstream.
|
|
290
|
+
if (typeof m.correlationId !== "string"
|
|
291
|
+
|| (m.correlationId as string).length === 0
|
|
292
|
+
|| (m.correlationId as string).length > 64) return false;
|
|
293
|
+
if (typeof m.agentName !== "string"
|
|
294
|
+
|| !AGENT_NAME_RE.test(m.agentName as string)) return false;
|
|
295
|
+
if (typeof m.preview !== "object" || m.preview === null) return false;
|
|
296
|
+
if (m.ttlMs !== undefined
|
|
297
|
+
&& (typeof m.ttlMs !== "number"
|
|
298
|
+
|| !Number.isFinite(m.ttlMs)
|
|
299
|
+
|| (m.ttlMs as number) < 0)) return false;
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
276
302
|
default:
|
|
277
303
|
return false;
|
|
278
304
|
}
|
|
@@ -292,6 +318,7 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
|
|
|
292
318
|
onPtyPartial,
|
|
293
319
|
onInjectInbound,
|
|
294
320
|
onRequestDriveApproval,
|
|
321
|
+
onRequestMs365Approval,
|
|
295
322
|
onRequestConfigApproval,
|
|
296
323
|
onRequestConfigFinalize,
|
|
297
324
|
log = () => {},
|
|
@@ -436,6 +463,38 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
|
|
|
436
463
|
}
|
|
437
464
|
}
|
|
438
465
|
break;
|
|
466
|
+
case "request_ms365_approval":
|
|
467
|
+
if (onRequestMs365Approval) {
|
|
468
|
+
onRequestMs365Approval(client, msg as RequestMs365ApprovalMessage).catch(
|
|
469
|
+
(err) => {
|
|
470
|
+
log(
|
|
471
|
+
`request_ms365_approval handler threw (client=${client.id}): ${(err as Error).message}`,
|
|
472
|
+
);
|
|
473
|
+
try {
|
|
474
|
+
client.send({
|
|
475
|
+
type: "ms365_approval_posted",
|
|
476
|
+
correlationId: (msg as RequestMs365ApprovalMessage).correlationId,
|
|
477
|
+
ok: false,
|
|
478
|
+
reason: `gateway handler error: ${(err as Error).message}`,
|
|
479
|
+
});
|
|
480
|
+
} catch {
|
|
481
|
+
/* best effort */
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
} else {
|
|
486
|
+
try {
|
|
487
|
+
client.send({
|
|
488
|
+
type: "ms365_approval_posted",
|
|
489
|
+
correlationId: (msg as RequestMs365ApprovalMessage).correlationId,
|
|
490
|
+
ok: false,
|
|
491
|
+
reason: "gateway not configured for MS-365 write approval",
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
/* best effort */
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
439
498
|
case "request_config_approval":
|
|
440
499
|
if (onRequestConfigApproval) {
|
|
441
500
|
onRequestConfigApproval(
|