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.
Files changed (159) hide show
  1. package/dist/agent-scheduler/index.js +4 -2
  2. package/dist/auth-broker/index.js +21 -3
  3. package/dist/cli/notion-write-pretool.mjs +4 -2
  4. package/dist/cli/switchroom.js +1410 -852
  5. package/dist/host-control/main.js +22 -4
  6. package/dist/vault/approvals/kernel-server.js +21 -3
  7. package/dist/vault/broker/server.js +48 -4
  8. package/package.json +4 -3
  9. package/profiles/_base/start.sh.hbs +148 -23
  10. package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
  11. package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
  12. package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
  13. package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
  14. package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
  15. package/telegram-plugin/gateway/boot-card.ts +169 -1
  16. package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
  17. package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
  18. package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
  19. package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
  20. package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
  21. package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
  22. package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
  23. package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
  24. package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
  25. package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
  26. package/telegram-plugin/gateway/gateway.ts +6403 -13456
  27. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
  28. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
  29. package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
  30. package/telegram-plugin/gateway/inbound-router.ts +400 -0
  31. package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
  32. package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
  33. package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
  34. package/telegram-plugin/gateway/model-command.ts +23 -0
  35. package/telegram-plugin/gateway/narrative-lane.ts +865 -0
  36. package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
  37. package/telegram-plugin/gateway/obligation-store.ts +37 -1
  38. package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
  39. package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
  40. package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
  41. package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
  42. package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
  43. package/telegram-plugin/gateway/secret-request-card.ts +45 -0
  44. package/telegram-plugin/gateway/stream-render.ts +2166 -0
  45. package/telegram-plugin/gateway/turn-end.ts +606 -0
  46. package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
  47. package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
  48. package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
  49. package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
  50. package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
  51. package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
  52. package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
  53. package/telegram-plugin/narrative-dedup.ts +24 -1
  54. package/telegram-plugin/narrative-flush.ts +2 -2
  55. package/telegram-plugin/pending-user-notice.ts +59 -13
  56. package/telegram-plugin/render/render.ts +25 -1
  57. package/telegram-plugin/status-no-truncate.ts +13 -0
  58. package/telegram-plugin/subagent-watcher.ts +297 -31
  59. package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
  60. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
  61. package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
  62. package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
  63. package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
  64. package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
  65. package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
  66. package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
  67. package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
  68. package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
  69. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +28 -9
  70. package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
  71. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
  72. package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
  73. package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
  74. package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
  75. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
  76. package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
  77. package/telegram-plugin/tests/emission-authority-facade.test.ts +76 -29
  78. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
  79. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
  80. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
  81. package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
  82. package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
  83. package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
  84. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
  85. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
  86. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
  87. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
  88. package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
  89. package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
  90. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
  91. package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
  92. package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
  93. package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
  94. package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
  95. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
  96. package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
  97. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
  98. package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
  99. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
  100. package/telegram-plugin/tests/model-command.test.ts +30 -0
  101. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +36 -12
  102. package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
  103. package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
  104. package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
  105. package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
  106. package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
  107. package/telegram-plugin/tests/obligation-store.test.ts +43 -0
  108. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
  109. package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
  110. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
  111. package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
  112. package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
  113. package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
  114. package/telegram-plugin/tests/render/render.test.ts +42 -0
  115. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
  116. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
  117. package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
  118. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +31 -20
  119. package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
  120. package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
  121. package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
  122. package/telegram-plugin/tests/stop-command.test.ts +22 -12
  123. package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
  124. package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
  125. package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +305 -0
  126. package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
  127. package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
  128. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
  129. package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
  130. package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
  131. package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
  132. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
  133. package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
  134. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
  135. package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
  136. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
  137. package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
  138. package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
  139. package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
  140. package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
  141. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +236 -20
  142. package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
  143. package/telegram-plugin/tool-activity-summary.ts +110 -38
  144. package/telegram-plugin/turn-flush-safety.ts +80 -14
  145. package/telegram-plugin/uat/restart-capability.ts +76 -0
  146. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
  147. package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
  148. package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
  149. package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
  150. package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
  151. package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
  152. package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
  153. package/telegram-plugin/worker-activity-feed.ts +34 -4
  154. package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
  155. package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
  156. package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
  157. package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
  158. package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
  159. 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
+ }