switchroom 0.14.45 → 0.14.47
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/notion-write-pretool.mjs +82 -82
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +499 -378
- package/dist/host-control/main.js +148 -148
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/examples/switchroom.yaml +26 -0
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +335 -225
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +127 -60
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +17 -0
- package/telegram-plugin/permission-title.ts +100 -1
- package/telegram-plugin/tests/permission-card-routing.test.ts +77 -0
- package/telegram-plugin/tests/permission-title.test.ts +79 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +84 -0
|
@@ -87,6 +87,35 @@ describe('naturalAction — MCP tools', () => {
|
|
|
87
87
|
'list files (Google Workspace)',
|
|
88
88
|
)
|
|
89
89
|
})
|
|
90
|
+
|
|
91
|
+
// Clarity fix: REST-wrapper MCP tools (brevo/meta/postiz via rest-server.mjs)
|
|
92
|
+
// take a `path` — surface it so "post (Brevo)" becomes "POST /smtp/email
|
|
93
|
+
// (Brevo)" and the operator can see WHICH endpoint is being written.
|
|
94
|
+
test('REST-wrapper write names the endpoint with an uppercased HTTP verb', () => {
|
|
95
|
+
expect(
|
|
96
|
+
naturalAction('mcp__brevo__post', JSON.stringify({ path: '/smtp/email', body: { to: 'x' } })),
|
|
97
|
+
).toBe('POST /smtp/email (Brevo)')
|
|
98
|
+
expect(
|
|
99
|
+
naturalAction('mcp__brevo__put', JSON.stringify({ path: '/contacts/123', body: {} })),
|
|
100
|
+
).toBe('PUT /contacts/123 (Brevo)')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('REST-wrapper read surfaces the path too', () => {
|
|
104
|
+
expect(
|
|
105
|
+
naturalAction('mcp__brevo__get', JSON.stringify({ path: '/contacts', query: { limit: 10 } })),
|
|
106
|
+
).toBe('GET /contacts (Brevo)')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('falls back to the plain verb phrase when there is no resource key', () => {
|
|
110
|
+
// No path → today's behavior, unchanged (defensive for unknown shapes).
|
|
111
|
+
expect(naturalAction('mcp__brevo__post', undefined)).toBe('post (Brevo)')
|
|
112
|
+
expect(naturalAction('mcp__brevo__post', JSON.stringify({ foo: 1 }))).toBe('post (Brevo)')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('internal REST-ish tool is NOT endpoint-enriched (stays a bare verb)', () => {
|
|
116
|
+
// hostd is internal → no "(Server)" tag, no path enrichment.
|
|
117
|
+
expect(naturalAction('mcp__hostd__do_thing', JSON.stringify({ path: '/x' }))).toBe('do thing')
|
|
118
|
+
})
|
|
90
119
|
})
|
|
91
120
|
|
|
92
121
|
describe('formatPermissionCardBody', () => {
|
|
@@ -156,6 +185,56 @@ describe('formatPermissionCardBody', () => {
|
|
|
156
185
|
})
|
|
157
186
|
expect(body).toContain('why: <i>first second paragraph</i>')
|
|
158
187
|
})
|
|
188
|
+
|
|
189
|
+
// Clarity fix: the card gains a third "↳" line summarizing the REST
|
|
190
|
+
// payload so the operator can see WHAT is being written, not just the
|
|
191
|
+
// endpoint. Values are redaction-passed + truncated; nested objects show
|
|
192
|
+
// as a bare key name.
|
|
193
|
+
test('REST write card: endpoint in the title + a payload summary line', () => {
|
|
194
|
+
const body = formatPermissionCardBody({
|
|
195
|
+
toolName: 'mcp__brevo__post',
|
|
196
|
+
inputPreview: JSON.stringify({
|
|
197
|
+
path: '/smtp/email',
|
|
198
|
+
body: { subject: 'Priority access', templateId: 12, to: [{ email: 'lisa@example.com' }] },
|
|
199
|
+
}),
|
|
200
|
+
description: 'HIGH RISK: write to the brevo API (POST).',
|
|
201
|
+
agentName: 'marko',
|
|
202
|
+
})
|
|
203
|
+
const lines = body.split('\n')
|
|
204
|
+
expect(lines[0]).toBe('🔐 <b>Marko</b> wants to POST /smtp/email (Brevo)')
|
|
205
|
+
expect(lines[1]).toBe('why: <i>HIGH RISK: write to the brevo API (POST).</i>')
|
|
206
|
+
// Third line: scalar keys show value; the nested `to` array shows key-only.
|
|
207
|
+
expect(lines[2]).toContain('↳')
|
|
208
|
+
expect(lines[2]).toContain('subject: Priority access')
|
|
209
|
+
expect(lines[2]).toContain('templateId: 12')
|
|
210
|
+
expect(lines[2]).toContain('to') // key-only, not the email object dumped
|
|
211
|
+
expect(lines[2]).not.toContain('lisa@example.com')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('no payload → no third line (DM / non-REST cards unchanged)', () => {
|
|
215
|
+
const body = formatPermissionCardBody({
|
|
216
|
+
toolName: 'Edit',
|
|
217
|
+
inputPreview: JSON.stringify({ file_path: '/a/b.md' }),
|
|
218
|
+
description: 'edit it',
|
|
219
|
+
agentName: 'clerk',
|
|
220
|
+
})
|
|
221
|
+
expect(body.split('\n')).toHaveLength(2)
|
|
222
|
+
expect(body).not.toContain('↳')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('redaction is load-bearing: a token in the payload is masked, never shown', () => {
|
|
226
|
+
// Build the fake token at runtime so the source file never holds a
|
|
227
|
+
// contiguous token literal (repo push-protection rule).
|
|
228
|
+
const fakeToken = 'sk-ant-' + 'api03-' + 'A'.repeat(48)
|
|
229
|
+
const body = formatPermissionCardBody({
|
|
230
|
+
toolName: 'mcp__brevo__post',
|
|
231
|
+
inputPreview: JSON.stringify({ path: '/contacts', body: { apiKey: fakeToken, name: 'Lisa' } }),
|
|
232
|
+
description: 'create a contact',
|
|
233
|
+
agentName: 'marko',
|
|
234
|
+
})
|
|
235
|
+
expect(body).not.toContain(fakeToken)
|
|
236
|
+
expect(body).toContain('name: Lisa') // benign value still surfaces
|
|
237
|
+
})
|
|
159
238
|
})
|
|
160
239
|
|
|
161
240
|
describe('describeGrant — phrased from the chosen scope', () => {
|
|
@@ -16,7 +16,11 @@ import { describe, it, expect } from 'vitest'
|
|
|
16
16
|
import {
|
|
17
17
|
buildVaultGrantApprovedInbound,
|
|
18
18
|
buildVaultGrantDeniedInbound,
|
|
19
|
+
buildVaultSaveCompletedInbound,
|
|
20
|
+
buildVaultSaveFailedInbound,
|
|
21
|
+
buildVaultSaveDiscardedInbound,
|
|
19
22
|
type VaultGrantInboundContext,
|
|
23
|
+
type VaultSaveInboundContext,
|
|
20
24
|
} from '../gateway/vault-grant-inbound-builders.js'
|
|
21
25
|
|
|
22
26
|
const FIXED_NOW = 1_700_000_000_000
|
|
@@ -224,3 +228,83 @@ describe('approve vs deny shape invariants', () => {
|
|
|
224
228
|
expect(String(deny.meta?.source)).toMatch(/^vault_grant_denied$/)
|
|
225
229
|
})
|
|
226
230
|
})
|
|
231
|
+
|
|
232
|
+
// ── Supergroup topic routing (gap #2) ──────────────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// When the agent requested the credential from inside a forum topic, the
|
|
235
|
+
// grant/save-outcome inbound must carry that topic so the resumed turn's
|
|
236
|
+
// reply lands back in it — not General. The carrier is two-fold and BOTH
|
|
237
|
+
// halves are load-bearing:
|
|
238
|
+
// - top-level `threadId` → the gateway's per-topic busy-key / deliver-
|
|
239
|
+
// until-acked keying (markClaudeBusyForInbound reads it).
|
|
240
|
+
// - `meta.message_thread_id` (stringified) → rendered into the
|
|
241
|
+
// `<channel message_thread_id="…">` XML, which session-tail's
|
|
242
|
+
// parseChannelMeta re-extracts to set currentTurn.sessionThreadId, which
|
|
243
|
+
// the reply tool defaults to. Drop either and the reply mis-routes.
|
|
244
|
+
//
|
|
245
|
+
// DM / non-topic requests must leave BOTH absent (an empty-string or 0
|
|
246
|
+
// thread is a Telegram 400 "message thread not found").
|
|
247
|
+
describe('grant/save outcome topic routing', () => {
|
|
248
|
+
const SAVE_CTX: VaultSaveInboundContext = {
|
|
249
|
+
agent: 'marko',
|
|
250
|
+
key: 'brevo/api-key',
|
|
251
|
+
chat_id: '-1001234567890',
|
|
252
|
+
}
|
|
253
|
+
const THREAD = 4242
|
|
254
|
+
|
|
255
|
+
const grantBuilders = [
|
|
256
|
+
{
|
|
257
|
+
name: 'approved',
|
|
258
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
259
|
+
buildVaultGrantApprovedInbound({ ctx, grantId: 'vg_x', stageId: 's', operatorId: '1' }),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'denied',
|
|
263
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
264
|
+
buildVaultGrantDeniedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
const saveBuilders = [
|
|
268
|
+
{
|
|
269
|
+
name: 'save-completed',
|
|
270
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
271
|
+
buildVaultSaveCompletedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'save-failed',
|
|
275
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
276
|
+
buildVaultSaveFailedInbound({ ctx, stageId: 's', operatorId: '1', reason: 'disk full' }),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'save-discarded',
|
|
280
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
281
|
+
buildVaultSaveDiscardedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
for (const { name, build } of grantBuilders) {
|
|
286
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
287
|
+
const msg = build({ ...CTX_READ, threadId: THREAD })
|
|
288
|
+
expect(msg.threadId).toBe(THREAD)
|
|
289
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
290
|
+
})
|
|
291
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
292
|
+
const msg = build(CTX_READ)
|
|
293
|
+
expect(msg.threadId).toBeUndefined()
|
|
294
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const { name, build } of saveBuilders) {
|
|
299
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
300
|
+
const msg = build({ ...SAVE_CTX, threadId: THREAD })
|
|
301
|
+
expect(msg.threadId).toBe(THREAD)
|
|
302
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
303
|
+
})
|
|
304
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
305
|
+
const msg = build(SAVE_CTX)
|
|
306
|
+
expect(msg.threadId).toBeUndefined()
|
|
307
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|