switchroom 0.18.31 → 0.18.33
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 +4 -2
- package/dist/auth-broker/index.js +21 -3
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1410 -852
- package/dist/host-control/main.js +22 -4
- package/dist/vault/approvals/kernel-server.js +21 -3
- package/dist/vault/broker/server.js +48 -4
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +148 -23
- package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +6403 -13456
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +297 -31
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +28 -9
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +76 -29
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +36 -12
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +31 -20
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +305 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +236 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +110 -38
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +34 -4
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo inbound handler (switchroom#2996 P6 cluster D).
|
|
3
|
+
*
|
|
4
|
+
* `message:photo` is the one attachment type whose gateway handling is not a
|
|
5
|
+
* pure metadata render: it downloads the largest photo size from the Telegram
|
|
6
|
+
* CDN (bounded 15s fetch), writes it into the per-agent inbox with 0600 perms,
|
|
7
|
+
* and hands the on-disk path to the coalescing inbound pipeline as the image
|
|
8
|
+
* download. Split out of the cluster-C metadata handlers precisely because it
|
|
9
|
+
* carries side-effecting IO + bot-token handling.
|
|
10
|
+
*
|
|
11
|
+
* The registration line stays in gateway.ts (order is a load-bearing invariant
|
|
12
|
+
* pinned by gateway-handler-registration-wiring.test.ts) and delegates here.
|
|
13
|
+
* The bot token is read lazily via `getToken()` — it is materialized after
|
|
14
|
+
* this deps object is constructed — and never exposed in logs (redacted on the
|
|
15
|
+
* error path). Path-safety helpers are imported directly.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import type { Context, Filter } from 'grammy'
|
|
20
|
+
import { buildAttachmentPath, assertInsideInbox } from '../attachment-path.js'
|
|
21
|
+
|
|
22
|
+
export interface PhotoHandlerDeps {
|
|
23
|
+
handleInboundCoalesced: (
|
|
24
|
+
ctx: Context,
|
|
25
|
+
text: string,
|
|
26
|
+
downloadImage: (() => Promise<string | undefined>) | undefined,
|
|
27
|
+
) => Promise<void>
|
|
28
|
+
/** Lazily read the materialized bot token (set after deps construction). */
|
|
29
|
+
getToken: () => string
|
|
30
|
+
/** Per-agent inbox directory (0700). */
|
|
31
|
+
inboxDir: string
|
|
32
|
+
/** stderr log sink. */
|
|
33
|
+
log: (line: string) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handlePhotoMessage(
|
|
37
|
+
ctx: Filter<Context, 'message:photo'>,
|
|
38
|
+
deps: PhotoHandlerDeps,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const caption = ctx.message.caption ?? '(photo)'
|
|
41
|
+
await deps.handleInboundCoalesced(ctx, caption, async () => {
|
|
42
|
+
const photos = ctx.message.photo
|
|
43
|
+
const best = photos[photos.length - 1]
|
|
44
|
+
try {
|
|
45
|
+
const file = await ctx.api.getFile(best.file_id)
|
|
46
|
+
if (!file.file_path) return undefined
|
|
47
|
+
// Build download URL — token is embedded in the URL but never exposed
|
|
48
|
+
// in error messages or logs (caught and sanitized below).
|
|
49
|
+
//
|
|
50
|
+
// Bounded fetch: a stalled Telegram CDN connection without a
|
|
51
|
+
// timeout would hang the entire inbound handler, blocking the
|
|
52
|
+
// user's photo from ever being acked or seen by the agent.
|
|
53
|
+
// 15s is generous for normal photos (typical 100ms-2s) and
|
|
54
|
+
// tight enough to surface a real outage.
|
|
55
|
+
const token = deps.getToken()
|
|
56
|
+
const downloadUrl = `https://api.telegram.org/file/bot${token}/${file.file_path}`
|
|
57
|
+
const res = await fetch(downloadUrl, { signal: AbortSignal.timeout(15_000) })
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
deps.log(`telegram gateway: photo download failed: HTTP ${res.status}\n`)
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
63
|
+
const dlPath = buildAttachmentPath({
|
|
64
|
+
inboxDir: deps.inboxDir,
|
|
65
|
+
telegramFilePath: file.file_path,
|
|
66
|
+
fileUniqueId: best.file_unique_id,
|
|
67
|
+
now: Date.now(),
|
|
68
|
+
})
|
|
69
|
+
mkdirSync(deps.inboxDir, { recursive: true, mode: 0o700 })
|
|
70
|
+
assertInsideInbox(deps.inboxDir, dlPath)
|
|
71
|
+
writeFileSync(dlPath, buf, { mode: 0o600 })
|
|
72
|
+
return dlPath
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Sanitize error to avoid leaking bot token in logs
|
|
75
|
+
const msg = err instanceof Error ? err.message : 'unknown error'
|
|
76
|
+
deps.log(`telegram gateway: photo download failed: ${msg.replace(deps.getToken(), '<REDACTED>')}\n`)
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `message:pinned_message` service-message cleanup (switchroom#2996 P6 cluster F).
|
|
3
|
+
*
|
|
4
|
+
* Suppresses the "pinned a message" service message Telegram inserts when OUR
|
|
5
|
+
* silent status-pin fires. The pin call passes `disable_notification: true`,
|
|
6
|
+
* which kills the PUSH notification but NOT the in-chat service message — so we
|
|
7
|
+
* delete that service message as it lands, but ONLY for pins we own (tracked in
|
|
8
|
+
* `statusPinState`). Manual/operator pins are never silent and are never
|
|
9
|
+
* touched. Only silent status pins reach here as an OUR-pin match, so the
|
|
10
|
+
* ownership check is the guard.
|
|
11
|
+
*
|
|
12
|
+
* The registration line stays in gateway.ts (order is a load-bearing invariant
|
|
13
|
+
* pinned by gateway-handler-registration-wiring.test.ts) and delegates here.
|
|
14
|
+
*
|
|
15
|
+
* DI note (check-bot-api-wrapping): the actual `lockedBot.api.deleteMessage`
|
|
16
|
+
* call is NOT made here — it is injected as `deleteServiceMessage`, bound in
|
|
17
|
+
* gateway.ts inside a `robustApiCall(...)` wrapper. This keeps the raw
|
|
18
|
+
* `lockedBot.api.*` callsite in gateway.ts (already inside the retry policy)
|
|
19
|
+
* and leaves this module with no raw Bot API site to allowlist.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Context, Filter } from 'grammy'
|
|
23
|
+
import { pinnedMessageIsOurs, type TrackedStatusPin } from './status-pin-store.js'
|
|
24
|
+
import type { PinState } from '../status-pin.js'
|
|
25
|
+
|
|
26
|
+
export interface PinnedMessageHandlerDeps {
|
|
27
|
+
/** Live pinKey→PinState registry of pins we own. */
|
|
28
|
+
statusPinState: ReadonlyMap<string, PinState>
|
|
29
|
+
/** Companion pinKey→chatId registry (chat-scoped ownership). */
|
|
30
|
+
statusPinChatIds: ReadonlyMap<string, string>
|
|
31
|
+
/**
|
|
32
|
+
* Delete the pin service message. Bound in gateway.ts through
|
|
33
|
+
* `robustApiCall(() => lockedBot.api.deleteMessage(...))` so the raw Bot API
|
|
34
|
+
* call stays inside the retry policy. Rethrows on failure (the caller logs).
|
|
35
|
+
*/
|
|
36
|
+
deleteServiceMessage: (chatId: string, messageId: number) => Promise<void>
|
|
37
|
+
/** stderr log sink. */
|
|
38
|
+
log: (line: string) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handlePinnedMessage(
|
|
42
|
+
ctx: Filter<Context, 'message:pinned_message'>,
|
|
43
|
+
deps: PinnedMessageHandlerDeps,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const pinnedId = ctx.msg.pinned_message?.message_id
|
|
46
|
+
if (pinnedId == null) return
|
|
47
|
+
const chatId = String(ctx.chat.id)
|
|
48
|
+
const serviceMsgId = ctx.msg.message_id
|
|
49
|
+
|
|
50
|
+
// Chat-scoped ownership (see pinnedMessageIsOurs): the match requires BOTH
|
|
51
|
+
// the messageId AND that the tracked entry lives in THIS chat, so a pin id
|
|
52
|
+
// colliding across chats can't delete a foreign (e.g. operator-manual) pin
|
|
53
|
+
// notice. statusPinChatIds is the companion pinKey→chatId map written on
|
|
54
|
+
// every desired-pinned reconcile.
|
|
55
|
+
const trackedPins = (): TrackedStatusPin[] => {
|
|
56
|
+
const out: TrackedStatusPin[] = []
|
|
57
|
+
for (const [pinKey, state] of deps.statusPinState) {
|
|
58
|
+
const c = deps.statusPinChatIds.get(pinKey)
|
|
59
|
+
if (c != null) out.push({ chatId: c, messageId: state.messageId })
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
const isOurs = () => pinnedMessageIsOurs(trackedPins(), chatId, pinnedId)
|
|
64
|
+
|
|
65
|
+
if (!isOurs()) {
|
|
66
|
+
// Tolerate the reconcile-store race: wait briefly, then re-check once.
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 250))
|
|
68
|
+
if (!isOurs()) return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await deps.deleteServiceMessage(chatId, serviceMsgId)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Best-effort: a failure to delete the service message is cosmetic only —
|
|
75
|
+
// the "pinned a message" line just stays. The most likely cause in a
|
|
76
|
+
// supergroup/forum is the bot lacking can_delete_messages admin right, so
|
|
77
|
+
// surface a concise one-liner (robustApiCall rethrows this case without
|
|
78
|
+
// logging a reason) rather than swallowing silently — an operator sees WHY.
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
80
|
+
deps.log(
|
|
81
|
+
`telegram gateway: status-pin: could not delete pin service message ` +
|
|
82
|
+
`(chat=${chatId} msg=${serviceMsgId}) — likely missing can_delete_messages ` +
|
|
83
|
+
`admin right in this chat: ${msg}\n`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
renderSecretRequestCard,
|
|
4
|
+
buildSecretRequestKeyboard,
|
|
5
|
+
} from './secret-request-card.js'
|
|
6
|
+
|
|
7
|
+
describe('secret-request-card (#2996 P5 extraction — verbatim from gateway.ts)', () => {
|
|
8
|
+
describe('renderSecretRequestCard', () => {
|
|
9
|
+
it('renders the title, key, reason, and the provide-securely instruction', () => {
|
|
10
|
+
const out = renderSecretRequestCard({
|
|
11
|
+
agent: 'worker',
|
|
12
|
+
key: 'openai/api_key',
|
|
13
|
+
reason: 'need it to call the API',
|
|
14
|
+
})
|
|
15
|
+
expect(out).toContain('🔒 **worker** needs a secret:')
|
|
16
|
+
expect(out).toContain('`openai/api_key`')
|
|
17
|
+
expect(out).toContain('_need it to call the API_')
|
|
18
|
+
expect(out).toContain('Tap **Provide securely**')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('omits the reason line when none is given', () => {
|
|
22
|
+
const out = renderSecretRequestCard({ agent: 'worker', key: 'openai/api_key' })
|
|
23
|
+
// The reason line is the only `_…_` emphasis block; absent when no reason.
|
|
24
|
+
expect(out).not.toMatch(/\n_.*_$/m)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('does NOT harden card breaks (prose body, preserved from the inline version)', () => {
|
|
28
|
+
// The inline renderer joined lines plainly (no hardenCardBreaks); this
|
|
29
|
+
// pins that difference from the vault-save card so the move stays verbatim.
|
|
30
|
+
const out = renderSecretRequestCard({ agent: 'worker', key: 'k/v' })
|
|
31
|
+
expect(out.split('\n')[0]).toBe('🔒 **worker** needs a secret:')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('buildSecretRequestKeyboard', () => {
|
|
36
|
+
it('carries the vsp:* callback_data contract', () => {
|
|
37
|
+
const kb = buildSecretRequestKeyboard('abcd1234')
|
|
38
|
+
const flat = kb.inline_keyboard.flat()
|
|
39
|
+
expect(flat.map((b) => b.callback_data)).toEqual([
|
|
40
|
+
'vsp:provide:abcd1234',
|
|
41
|
+
'vsp:decline:abcd1234',
|
|
42
|
+
])
|
|
43
|
+
expect(flat.map((b) => b.text)).toEqual(['🔐 Provide securely', '🚫 Decline'])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure renderer + inline-keyboard builder for the agent-initiated
|
|
3
|
+
* `request_secret` card (#2045 — the agent does NOT have a value and asks the
|
|
4
|
+
* operator to PROVIDE one securely). Callback prefix `vsp:`
|
|
5
|
+
* (vault-secret-provide), handled by handleSecretRequestCallback in
|
|
6
|
+
* callback-query-handlers.ts.
|
|
7
|
+
*
|
|
8
|
+
* Extracted verbatim from gateway.ts (#2996 P5); the render body is unchanged.
|
|
9
|
+
* Unlike the vault-save card this renderer does NOT call hardenCardBreaks — the
|
|
10
|
+
* body is a short prose block, not labelled field lines — so the move preserves
|
|
11
|
+
* that difference exactly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { escapeHtmlForTg } from '../shared/bot-runtime.js'
|
|
15
|
+
|
|
16
|
+
/** Minimal shape the card needs — a subset of PendingSecretRequest. */
|
|
17
|
+
export interface SecretRequestCardInput {
|
|
18
|
+
agent: string
|
|
19
|
+
key: string
|
|
20
|
+
reason?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderSecretRequestCard(req: SecretRequestCardInput): string {
|
|
24
|
+
const lines: string[] = [
|
|
25
|
+
`🔒 **${escapeHtmlForTg(req.agent)}** needs a secret:`,
|
|
26
|
+
`\`${req.key}\``,
|
|
27
|
+
]
|
|
28
|
+
if (req.reason) lines.push(`_${escapeHtmlForTg(req.reason)}_`)
|
|
29
|
+
lines.push(
|
|
30
|
+
'',
|
|
31
|
+
'Tap **Provide securely**, then send the value as your next message. I’ll delete it instantly and store it in the vault — it is never shown in chat or to the agent.',
|
|
32
|
+
)
|
|
33
|
+
return lines.join('\n')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildSecretRequestKeyboard(stageId: string): { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> } {
|
|
37
|
+
return {
|
|
38
|
+
inline_keyboard: [
|
|
39
|
+
[
|
|
40
|
+
{ text: '🔐 Provide securely', callback_data: `vsp:provide:${stageId}` },
|
|
41
|
+
{ text: '🚫 Decline', callback_data: `vsp:decline:${stageId}` },
|
|
42
|
+
],
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
}
|