switchroom 0.16.29 → 0.16.46
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 +101 -84
- package/dist/auth-broker/index.js +100 -83
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +102 -85
- package/dist/cli/skill-validate-pretool.mjs +91 -91
- package/dist/cli/switchroom.js +62124 -60406
- package/dist/cli/ui/index.html +877 -214
- package/dist/host-control/main.js +1000 -256
- package/dist/vault/approvals/kernel-server.js +141 -121
- package/dist/vault/broker/server.js +163 -143
- package/examples/minimal.yaml +1 -1
- package/examples/switchroom.yaml +1 -1
- package/package.json +3 -2
- package/profiles/_shared/agent-self-service.md.hbs +7 -2
- package/profiles/_shared/reply-discipline.md.hbs +9 -0
- package/skills/switchroom-status/SKILL.md +1 -1
- package/telegram-plugin/auth-snapshot-format.ts +173 -67
- package/telegram-plugin/auto-fallback-fleet.ts +3 -6
- package/telegram-plugin/bridge/bridge.ts +2 -1
- package/telegram-plugin/card-format.ts +59 -3
- package/telegram-plugin/credits-watch.ts +4 -7
- package/telegram-plugin/dist/bridge/bridge.js +132 -114
- package/telegram-plugin/dist/gateway/gateway.js +4456 -1591
- package/telegram-plugin/dist/server.js +180 -163
- package/telegram-plugin/format.ts +551 -20
- package/telegram-plugin/gateway/approval-card.ts +7 -14
- package/telegram-plugin/gateway/approvals-commands.ts +6 -9
- package/telegram-plugin/gateway/auth-command.ts +35 -38
- package/telegram-plugin/gateway/boot-card.ts +7 -2
- package/telegram-plugin/gateway/chat-id-fallback.ts +46 -0
- package/telegram-plugin/gateway/config-approval-handler.ts +6 -9
- package/telegram-plugin/gateway/diff-preview-card.ts +3 -6
- package/telegram-plugin/gateway/gateway.ts +1524 -129
- package/telegram-plugin/gateway/ipc-protocol.ts +66 -2
- package/telegram-plugin/gateway/ipc-server.ts +91 -1
- package/telegram-plugin/gateway/linear-activity.ts +2 -5
- package/telegram-plugin/gateway/model-command.ts +173 -19
- package/telegram-plugin/gateway/obligation-turn-end.ts +27 -0
- package/telegram-plugin/gateway/permission-card-store.ts +104 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -6
- package/telegram-plugin/gateway/status-pin-store.ts +302 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +30 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +12 -1
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +35 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +61 -0
- package/telegram-plugin/history.ts +17 -7
- package/telegram-plugin/hooks/tool-label-pretool.d.mts +12 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +54 -16
- package/telegram-plugin/idle-footer.ts +2 -2
- package/telegram-plugin/issues-card.ts +12 -7
- package/telegram-plugin/model-unavailable.ts +3 -6
- package/telegram-plugin/operator-events.ts +4 -6
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/quota-check.ts +2 -2
- package/telegram-plugin/quota-watch.ts +7 -10
- package/telegram-plugin/server.ts +3 -1
- package/telegram-plugin/session-tail.ts +47 -1
- package/telegram-plugin/status-pin-driver.ts +102 -0
- package/telegram-plugin/status-pin.ts +76 -0
- package/telegram-plugin/stream-reply-handler.ts +33 -2
- package/telegram-plugin/subagent-watcher.ts +6 -3
- package/telegram-plugin/tests/always-allow-grant.test.ts +34 -2
- package/telegram-plugin/tests/auth-command-format2.test.ts +6 -2
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +18 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +204 -45
- package/telegram-plugin/tests/card-format.test.ts +79 -0
- package/telegram-plugin/tests/chat-id-fallback.test.ts +74 -0
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +151 -0
- package/telegram-plugin/tests/codespan-escaping-golden.test.ts +166 -0
- package/telegram-plugin/tests/credits-watch.test.ts +18 -0
- package/telegram-plugin/tests/format-consistency.test.ts +223 -0
- package/telegram-plugin/tests/formatting-parse-regression.test.ts +272 -0
- package/telegram-plugin/tests/formatting-torture-set.ts +218 -0
- package/telegram-plugin/tests/history.test.ts +38 -2
- package/telegram-plugin/tests/idle-footer.test.ts +53 -9
- package/telegram-plugin/tests/ipc-server-validate-rollout-status.test.ts +66 -0
- package/telegram-plugin/tests/issues-card.test.ts +24 -0
- package/telegram-plugin/tests/model-command.test.ts +213 -47
- package/telegram-plugin/tests/obligation-turn-end.test.ts +76 -0
- package/telegram-plugin/tests/paragraph-normalizer.test.ts +384 -3
- package/telegram-plugin/tests/permission-card-single-edit.test.ts +66 -0
- package/telegram-plugin/tests/permission-card-store.test.ts +83 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +37 -6
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +23 -6
- package/telegram-plugin/tests/quota-check.test.ts +9 -2
- package/telegram-plugin/tests/quota-watch.test.ts +7 -7
- package/telegram-plugin/tests/rich-markdown-oracle.ts +469 -0
- package/telegram-plugin/tests/rollout-status-wiring.test.ts +81 -0
- package/telegram-plugin/tests/session-tail.test.ts +91 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +294 -0
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +210 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +450 -0
- package/telegram-plugin/tests/status-pin.test.ts +202 -0
- package/telegram-plugin/tests/status-vocabulary-unification.test.ts +125 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +39 -0
- package/telegram-plugin/tests/telegram-format.test.ts +121 -8
- package/telegram-plugin/tests/text-voice-scrub.test.ts +142 -22
- package/telegram-plugin/tests/tool-activity-summary.test.ts +88 -25
- package/telegram-plugin/tests/tts-normalize.test.ts +242 -0
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +39 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +12 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +54 -0
- package/telegram-plugin/tests/vault-request-access-card.test.ts +94 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +24 -0
- package/telegram-plugin/tests/voice-normalize-text.test.ts +256 -0
- package/telegram-plugin/tests/voice-ondemand.test.ts +299 -0
- package/telegram-plugin/tests/voice-out-one-send.test.ts +153 -0
- package/telegram-plugin/tests/voice-presynth.test.ts +437 -0
- package/telegram-plugin/tests/voice-synthesize-sidecar.test.ts +352 -0
- package/telegram-plugin/tests/voice-transcribe-sidecar.test.ts +332 -0
- package/telegram-plugin/tests/voice-transcribe.test.ts +188 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +77 -10
- package/telegram-plugin/text-voice-scrub.ts +68 -18
- package/telegram-plugin/tool-activity-summary.ts +31 -116
- package/telegram-plugin/tts-normalize.ts +377 -0
- package/telegram-plugin/uat/driver.ts +474 -17
- package/telegram-plugin/uat/scenarios/jtbd-model-litellm-sr-dm.test.ts +34 -14
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +169 -0
- package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +134 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +254 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-phase-transitions-dm.test.ts +109 -0
- package/telegram-plugin/uat/uat-driver.test.ts +297 -0
- package/telegram-plugin/voice-normalize-text.ts +340 -0
- package/telegram-plugin/voice-ondemand.ts +289 -0
- package/telegram-plugin/voice-presynth.ts +242 -0
- package/telegram-plugin/voice-synthesize-sidecar.ts +259 -0
- package/telegram-plugin/voice-synthesize.ts +128 -0
- package/telegram-plugin/voice-transcribe-sidecar.ts +176 -0
- package/telegram-plugin/worker-activity-feed.ts +22 -5
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
* param = (for ttl) 1h | 24h | 7d
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { escapeMarkdown, codeSpanSafe } from '../format.js';
|
|
17
18
|
import { InlineKeyboard } from "grammy";
|
|
18
19
|
|
|
20
|
+
// Re-export so existing importers (e.g. vault-request-access-card.ts) keep
|
|
21
|
+
// resolving `codeSpanSafe` from here; the canonical impl lives in format.ts.
|
|
22
|
+
export { codeSpanSafe };
|
|
23
|
+
|
|
19
24
|
export interface ApprovalCardOptions {
|
|
20
25
|
request_id: string; // 8-hex from kernel.requestApproval
|
|
21
26
|
agent: string; // shown in the title
|
|
@@ -37,7 +42,7 @@ export interface BuiltApprovalCard {
|
|
|
37
42
|
*/
|
|
38
43
|
export function buildApprovalCard(opts: ApprovalCardOptions): BuiltApprovalCard {
|
|
39
44
|
const lines: string[] = [];
|
|
40
|
-
lines.push(`🔐 **${
|
|
45
|
+
lines.push(`🔐 **${escapeMarkdown(opts.agent)}** wants approval`);
|
|
41
46
|
// The scope rides in a `code span` — content there is LITERAL, so it must
|
|
42
47
|
// NOT be markdown-escaped (escaping would leak visible backslashes, e.g.
|
|
43
48
|
// `secret:OPENAI\_API\_KEY`). Only an embedded backtick could break out of
|
|
@@ -45,7 +50,7 @@ export function buildApprovalCard(opts: ApprovalCardOptions): BuiltApprovalCard
|
|
|
45
50
|
lines.push(`\`${codeSpanSafe(opts.scope_humanized)}\``);
|
|
46
51
|
if (opts.why && opts.why.trim().length > 0) {
|
|
47
52
|
lines.push("");
|
|
48
|
-
lines.push(
|
|
53
|
+
lines.push(escapeMarkdown(opts.why.trim()));
|
|
49
54
|
}
|
|
50
55
|
const text = lines.join("\n");
|
|
51
56
|
|
|
@@ -123,15 +128,3 @@ export function ttlMsFromToken(token: string): number | null {
|
|
|
123
128
|
return null;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
function escapeHtml(s: string): string {
|
|
127
|
-
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Make a string safe to interpolate INSIDE a `code span` without escaping
|
|
132
|
-
* (content there is literal). The only character that can prematurely close
|
|
133
|
-
* the span is a backtick, so split it with a zero-width space.
|
|
134
|
-
*/
|
|
135
|
-
function codeSpanSafe(s: string): string {
|
|
136
|
-
return s.replace(/`/g, "`");
|
|
137
|
-
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* add on top of the same client. Tracked in the migration TODO inline.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { escapeMarkdown, codeSpanSafe } from '../format.js';
|
|
16
17
|
import type { Bot, Context } from "grammy";
|
|
17
18
|
import { richMessage } from "../rich-send.js";
|
|
18
19
|
import {
|
|
@@ -65,7 +66,7 @@ export function registerApprovalsCommands(
|
|
|
65
66
|
const byAgent = new Map<string, number>();
|
|
66
67
|
for (const d of decisions) byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
|
|
67
68
|
const summary = Array.from(byAgent.entries())
|
|
68
|
-
.map(([a, n]) => `• **${
|
|
69
|
+
.map(([a, n]) => `• **${escapeMarkdown(a)}**: ${n}`)
|
|
69
70
|
.join("\n");
|
|
70
71
|
const detail = decisions
|
|
71
72
|
.slice(0, 20)
|
|
@@ -76,10 +77,10 @@ export function registerApprovalsCommands(
|
|
|
76
77
|
: `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
|
|
77
78
|
return (
|
|
78
79
|
`\`${d.id.slice(0, 8)}\` ` +
|
|
79
|
-
`${
|
|
80
|
-
`\`${d.scope}\` ` +
|
|
81
|
-
`(${
|
|
82
|
-
`· /approvals revoke ${
|
|
80
|
+
`${escapeMarkdown(d.agent_unit)} → ` +
|
|
81
|
+
`\`${codeSpanSafe(d.scope)}\` ` +
|
|
82
|
+
`(${escapeMarkdown(d.action)}, ${ttl}) ` +
|
|
83
|
+
`· /approvals revoke ${escapeMarkdown(d.id)}`
|
|
83
84
|
);
|
|
84
85
|
})
|
|
85
86
|
.join("\n");
|
|
@@ -114,7 +115,3 @@ export function registerApprovalsCommands(
|
|
|
114
115
|
));
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
function escapeHtml(s: string): string {
|
|
119
|
-
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
120
|
-
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* rather than throwing.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { escapeMarkdown, codeSpanSafe } from '../format.js'
|
|
31
32
|
import type { ListStateData, AccountState } from './auth-line.js'
|
|
32
33
|
import {
|
|
33
34
|
buildSnapshotsFromState,
|
|
@@ -158,7 +159,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
158
159
|
if (tail.length > 0) {
|
|
159
160
|
return {
|
|
160
161
|
kind: 'help',
|
|
161
|
-
reason: `Unknown \`rm\` modifier: \`${
|
|
162
|
+
reason: `Unknown \`rm\` modifier: \`${codeSpanSafe(tail)}\`. Use \`/auth rm <label> confirm\` to confirm.`,
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
return { kind: 'rm-prompt', label }
|
|
@@ -180,7 +181,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
180
181
|
if (sub !== 'override') {
|
|
181
182
|
return {
|
|
182
183
|
kind: 'help',
|
|
183
|
-
reason: `Unknown \`agent\` subcommand: \`${
|
|
184
|
+
reason: `Unknown \`agent\` subcommand: \`${codeSpanSafe(sub || '(none)')}\`. Try \`/auth agent override <agent> <label|clear>\`.`,
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
const agent = parts[2]
|
|
@@ -201,7 +202,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
201
202
|
case 'help':
|
|
202
203
|
return { kind: 'help' }
|
|
203
204
|
default:
|
|
204
|
-
return { kind: 'help', reason: `Unknown verb: \`${
|
|
205
|
+
return { kind: 'help', reason: `Unknown verb: \`${codeSpanSafe(verb)}\`` }
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
@@ -417,7 +418,7 @@ export async function handleAuthCommand(
|
|
|
417
418
|
}
|
|
418
419
|
} catch (err) {
|
|
419
420
|
return {
|
|
420
|
-
text: `**/auth show failed:** ${
|
|
421
|
+
text: `**/auth show failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
421
422
|
html: true,
|
|
422
423
|
}
|
|
423
424
|
}
|
|
@@ -432,7 +433,7 @@ export async function handleAuthCommand(
|
|
|
432
433
|
if (!agent) {
|
|
433
434
|
return {
|
|
434
435
|
text:
|
|
435
|
-
`**/auth show:** no agent named \`${
|
|
436
|
+
`**/auth show:** no agent named \`${codeSpanSafe(agentName)}\` in broker view.\n` +
|
|
436
437
|
`Run \`/auth show\` for the fleet snapshot.`,
|
|
437
438
|
html: true,
|
|
438
439
|
}
|
|
@@ -440,7 +441,7 @@ export async function handleAuthCommand(
|
|
|
440
441
|
return { text: renderAgentDetail(state, agent), html: true }
|
|
441
442
|
} catch (err) {
|
|
442
443
|
return {
|
|
443
|
-
text: `**/auth show failed:** ${
|
|
444
|
+
text: `**/auth show failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
444
445
|
html: true,
|
|
445
446
|
}
|
|
446
447
|
}
|
|
@@ -476,13 +477,13 @@ export async function handleAuthCommand(
|
|
|
476
477
|
const result = await ctx.client.setActive(parsed.label)
|
|
477
478
|
return {
|
|
478
479
|
text:
|
|
479
|
-
`**Active account →** \`${
|
|
480
|
+
`**Active account →** \`${codeSpanSafe(result.active)}\`\n` +
|
|
480
481
|
`Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
|
|
481
482
|
html: true,
|
|
482
483
|
}
|
|
483
484
|
} catch (err) {
|
|
484
485
|
return {
|
|
485
|
-
text: `**/auth use failed:** ${
|
|
486
|
+
text: `**/auth use failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
486
487
|
html: true,
|
|
487
488
|
}
|
|
488
489
|
}
|
|
@@ -504,13 +505,13 @@ export async function handleAuthCommand(
|
|
|
504
505
|
const result = await ctx.client.setActive(nextLabel)
|
|
505
506
|
return {
|
|
506
507
|
text:
|
|
507
|
-
`**Rotated:** active → \`${
|
|
508
|
+
`**Rotated:** active → \`${codeSpanSafe(result.active)}\`\n` +
|
|
508
509
|
`Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
|
|
509
510
|
html: true,
|
|
510
511
|
}
|
|
511
512
|
} catch (err) {
|
|
512
513
|
return {
|
|
513
|
-
text: `**/auth rotate failed:** ${
|
|
514
|
+
text: `**/auth rotate failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
514
515
|
html: true,
|
|
515
516
|
}
|
|
516
517
|
}
|
|
@@ -525,7 +526,7 @@ export async function handleAuthCommand(
|
|
|
525
526
|
state = await ctx.client.listState()
|
|
526
527
|
} catch (err) {
|
|
527
528
|
return {
|
|
528
|
-
text: `**/auth rm failed:** ${
|
|
529
|
+
text: `**/auth rm failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
529
530
|
html: true,
|
|
530
531
|
}
|
|
531
532
|
}
|
|
@@ -533,7 +534,7 @@ export async function handleAuthCommand(
|
|
|
533
534
|
if (!exists) {
|
|
534
535
|
return {
|
|
535
536
|
text:
|
|
536
|
-
`**/auth rm:** no account named \`${
|
|
537
|
+
`**/auth rm:** no account named \`${codeSpanSafe(parsed.label)}\`. ` +
|
|
537
538
|
`Run \`/auth show\` for the current list.`,
|
|
538
539
|
html: true,
|
|
539
540
|
}
|
|
@@ -541,7 +542,7 @@ export async function handleAuthCommand(
|
|
|
541
542
|
if (state.active === parsed.label) {
|
|
542
543
|
return {
|
|
543
544
|
text:
|
|
544
|
-
`**/auth rm refused.** \`${
|
|
545
|
+
`**/auth rm refused.** \`${codeSpanSafe(parsed.label)}\` is the fleet active. ` +
|
|
545
546
|
`Switch with \`/auth use <other>\` or \`/auth rotate\` first.`,
|
|
546
547
|
html: true,
|
|
547
548
|
}
|
|
@@ -560,9 +561,9 @@ export async function handleAuthCommand(
|
|
|
560
561
|
}
|
|
561
562
|
return {
|
|
562
563
|
text:
|
|
563
|
-
`**⚠ /auth rm** — about to remove \`${
|
|
564
|
-
`The fleet active is unchanged. Any agent override pointing at \`${
|
|
565
|
-
`Send \`/auth rm ${
|
|
564
|
+
`**⚠ /auth rm** — about to remove \`${codeSpanSafe(parsed.label)}\` from the broker.\n` +
|
|
565
|
+
`The fleet active is unchanged. Any agent override pointing at \`${codeSpanSafe(parsed.label)}\` will stop working.\n\n` +
|
|
566
|
+
`Send \`/auth rm ${codeSpanSafe(parsed.label)} confirm\` within ${Math.round(
|
|
566
567
|
AUTH_RM_CONFIRM_TTL_MS / 1000,
|
|
567
568
|
)}s to proceed.`,
|
|
568
569
|
html: true,
|
|
@@ -578,8 +579,8 @@ export async function handleAuthCommand(
|
|
|
578
579
|
}
|
|
579
580
|
return {
|
|
580
581
|
text:
|
|
581
|
-
`**/auth rm:** no pending confirm for \`${
|
|
582
|
-
`Send \`/auth rm ${
|
|
582
|
+
`**/auth rm:** no pending confirm for \`${codeSpanSafe(parsed.label)}\` (expired or not started). ` +
|
|
583
|
+
`Send \`/auth rm ${codeSpanSafe(parsed.label)}\` first.`,
|
|
583
584
|
html: true,
|
|
584
585
|
}
|
|
585
586
|
}
|
|
@@ -589,12 +590,12 @@ export async function handleAuthCommand(
|
|
|
589
590
|
try {
|
|
590
591
|
const data = await ctx.client.rmAccount(parsed.label)
|
|
591
592
|
return {
|
|
592
|
-
text: `**Removed** \`${
|
|
593
|
+
text: `**Removed** \`${codeSpanSafe(data.label)}\` from the broker.`,
|
|
593
594
|
html: true,
|
|
594
595
|
}
|
|
595
596
|
} catch (err) {
|
|
596
597
|
return {
|
|
597
|
-
text: `**/auth rm failed:** ${
|
|
598
|
+
text: `**/auth rm failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
598
599
|
html: true,
|
|
599
600
|
}
|
|
600
601
|
}
|
|
@@ -609,7 +610,7 @@ export async function handleAuthCommand(
|
|
|
609
610
|
if (parsed.label && targets.length === 0) {
|
|
610
611
|
return {
|
|
611
612
|
text:
|
|
612
|
-
`**/auth refresh:** no account named \`${
|
|
613
|
+
`**/auth refresh:** no account named \`${codeSpanSafe(parsed.label)}\`.`,
|
|
613
614
|
html: true,
|
|
614
615
|
}
|
|
615
616
|
}
|
|
@@ -629,13 +630,13 @@ export async function handleAuthCommand(
|
|
|
629
630
|
])
|
|
630
631
|
} catch (err) {
|
|
631
632
|
failures.push(
|
|
632
|
-
`${label}: ${
|
|
633
|
+
`${label}: ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
633
634
|
)
|
|
634
635
|
}
|
|
635
636
|
}
|
|
636
637
|
const head =
|
|
637
638
|
targets.length === 1
|
|
638
|
-
? `**Refreshed** \`${
|
|
639
|
+
? `**Refreshed** \`${codeSpanSafe(targets[0]!)}\``
|
|
639
640
|
: `**Refreshed** ${rows.length - 1}/${targets.length} account${targets.length === 1 ? '' : 's'}`
|
|
640
641
|
const table = rows.length > 1
|
|
641
642
|
? "\n```\n" + alignTable(rows) + "\n```"
|
|
@@ -646,7 +647,7 @@ export async function handleAuthCommand(
|
|
|
646
647
|
return { text: head + table + failBlock, html: true }
|
|
647
648
|
} catch (err) {
|
|
648
649
|
return {
|
|
649
|
-
text: `**/auth refresh failed:** ${
|
|
650
|
+
text: `**/auth refresh failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
650
651
|
html: true,
|
|
651
652
|
}
|
|
652
653
|
}
|
|
@@ -657,13 +658,13 @@ export async function handleAuthCommand(
|
|
|
657
658
|
const data = await ctx.client.setOverride(parsed.agent, parsed.label)
|
|
658
659
|
return {
|
|
659
660
|
text:
|
|
660
|
-
`**Override set.** \`${
|
|
661
|
-
`\`${
|
|
661
|
+
`**Override set.** \`${codeSpanSafe(data.agent)}\` is now pinned to ` +
|
|
662
|
+
`\`${codeSpanSafe(data.account ?? parsed.label)}\`.`,
|
|
662
663
|
html: true,
|
|
663
664
|
}
|
|
664
665
|
} catch (err) {
|
|
665
666
|
return {
|
|
666
|
-
text: `**/auth agent override failed:** ${
|
|
667
|
+
text: `**/auth agent override failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
667
668
|
html: true,
|
|
668
669
|
}
|
|
669
670
|
}
|
|
@@ -674,13 +675,13 @@ export async function handleAuthCommand(
|
|
|
674
675
|
const data = await ctx.client.setOverride(parsed.agent, null)
|
|
675
676
|
return {
|
|
676
677
|
text:
|
|
677
|
-
`**Override cleared** on \`${
|
|
678
|
+
`**Override cleared** on \`${codeSpanSafe(data.agent)}\` ` +
|
|
678
679
|
`— back to fleet active.`,
|
|
679
680
|
html: true,
|
|
680
681
|
}
|
|
681
682
|
} catch (err) {
|
|
682
683
|
return {
|
|
683
|
-
text: `**/auth agent override failed:** ${
|
|
684
|
+
text: `**/auth agent override failed:** ${escapeMarkdown((err as Error)?.message ?? String(err))}`,
|
|
684
685
|
html: true,
|
|
685
686
|
}
|
|
686
687
|
}
|
|
@@ -852,7 +853,7 @@ function formatAccountsTable(state: ListStateData, now: number, demo = false): s
|
|
|
852
853
|
? formatRelativeMs(acc.exhausted_until - now)
|
|
853
854
|
: '—'
|
|
854
855
|
rows.push([
|
|
855
|
-
`${marker} ${
|
|
856
|
+
`${marker} ${escapeMarkdown(demo ? maskEmail(acc.label) : acc.label)}`,
|
|
856
857
|
status,
|
|
857
858
|
expires,
|
|
858
859
|
formatQuotaUtilCell(acc, now),
|
|
@@ -892,7 +893,7 @@ function formatAgentsTable(state: ListStateData, demo = false): string {
|
|
|
892
893
|
: a.account === state.active
|
|
893
894
|
? 'fleet-active'
|
|
894
895
|
: 'pinned'
|
|
895
|
-
rows.push([
|
|
896
|
+
rows.push([escapeMarkdown(a.name), escapeMarkdown(demo ? maskEmail(a.account) : a.account), source])
|
|
896
897
|
}
|
|
897
898
|
return alignTable(rows)
|
|
898
899
|
}
|
|
@@ -909,10 +910,10 @@ export function renderAgentDetail(
|
|
|
909
910
|
now: number = Date.now(),
|
|
910
911
|
): string {
|
|
911
912
|
const lines: string[] = []
|
|
912
|
-
lines.push(`**${
|
|
913
|
+
lines.push(`**${escapeMarkdown(agent.name)}**`)
|
|
913
914
|
const source = agent.override ? 'override' : 'fleet-active'
|
|
914
915
|
lines.push(
|
|
915
|
-
`Active account: \`${
|
|
916
|
+
`Active account: \`${codeSpanSafe(agent.account)}\` (${source})`,
|
|
916
917
|
)
|
|
917
918
|
const acct = state.accounts.find((a) => a.label === agent.account)
|
|
918
919
|
if (acct) {
|
|
@@ -948,17 +949,13 @@ function formatConsumersTable(state: ListStateData, now: number, demo = false):
|
|
|
948
949
|
c.last_seen_at == null
|
|
949
950
|
? 'socket bound'
|
|
950
951
|
: `socket bound (last seen ${formatRelativeMs(now - c.last_seen_at)} ago)`
|
|
951
|
-
rows.push([
|
|
952
|
+
rows.push([escapeMarkdown(c.name), escapeMarkdown(demo ? maskEmail(c.account) : c.account), status])
|
|
952
953
|
}
|
|
953
954
|
return alignTable(rows)
|
|
954
955
|
}
|
|
955
956
|
|
|
956
957
|
// ─── Plain-text helpers ────────────────────────────────────────────────────
|
|
957
958
|
|
|
958
|
-
function escapeHtml(s: string): string {
|
|
959
|
-
return s.replace(/([\\`*_~=\[\]|])/g, '\\$1')
|
|
960
|
-
}
|
|
961
|
-
|
|
962
959
|
function formatRelativeMs(ms: number): string {
|
|
963
960
|
if (ms <= 0) return '0s'
|
|
964
961
|
const totalSec = Math.floor(ms / 1000)
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
AGENT_LIVE_WINDOW_MS,
|
|
52
52
|
AGENT_LIVE_POLL_INTERVAL_MS,
|
|
53
53
|
} from './boot-probes.js'
|
|
54
|
-
import { escapeMarkdown } from '../card-format.js'
|
|
54
|
+
import { escapeMarkdown, stackCardLines } from '../card-format.js'
|
|
55
55
|
import {
|
|
56
56
|
loadCache as loadBootIssueCache,
|
|
57
57
|
diffProbes as diffBootProbes,
|
|
@@ -468,7 +468,12 @@ export function renderBootCard(opts: RenderBootCardOpts): string {
|
|
|
468
468
|
sections.push('', ...configChangeRows)
|
|
469
469
|
}
|
|
470
470
|
if (sections.length === 1) return ack
|
|
471
|
-
|
|
471
|
+
// Stack rows with GFM hard breaks so the boot card's styled prose rows don't
|
|
472
|
+
// collapse onto one visual line in the rich-message renderer (#2669). A
|
|
473
|
+
// section entry may itself carry an internal newline (a multi-line `ack`, an
|
|
474
|
+
// update-outcome blob), so flatten to single lines first — see stackCardLines.
|
|
475
|
+
const flatLines = sections.flatMap((s) => s.split('\n'))
|
|
476
|
+
return stackCardLines(flatLines)
|
|
472
477
|
}
|
|
473
478
|
|
|
474
479
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chat_id allowlist fallback for the reply / stream_reply final-answer
|
|
3
|
+
* paths.
|
|
4
|
+
*
|
|
5
|
+
* Non-Claude models (Gemini via LiteLLM `sr-*` routing) sometimes pass a
|
|
6
|
+
* chat_id that fails the allowlist:
|
|
7
|
+
*
|
|
8
|
+
* - **Bug A (numeric coercion):** the model passes a JSON number, so the
|
|
9
|
+
* caller must `String(...)`-coerce before the allowlist compare (the
|
|
10
|
+
* allowlist is a string set). Without coercion `42 !== "42"` and every
|
|
11
|
+
* reply looks un-allowlisted.
|
|
12
|
+
* - **Bug B (executeReply fallback):** the raw value is wrong, so fall back
|
|
13
|
+
* to the *active* turn's already-validated `sessionChatId`.
|
|
14
|
+
* - **Bug D (lastActiveTurnChatId survives silence poke):** a ≥5-min silence
|
|
15
|
+
* poke fires `clearTurnStarted`, so `currentTurn` is null by the time the
|
|
16
|
+
* model finally replies. The active-turn tier is gone, but the last-known
|
|
17
|
+
* turn's chat still routes the answer correctly.
|
|
18
|
+
*
|
|
19
|
+
* These hotfixes shipped test-free; this helper isolates the pure decision so
|
|
20
|
+
* each bug gets a focused regression test.
|
|
21
|
+
*
|
|
22
|
+
* Returns the resolved chat_id (rawChatId unchanged when no valid fallback
|
|
23
|
+
* exists, so the caller's `assertAllowedChat` throws the human-readable error)
|
|
24
|
+
* plus the tier used, for logging. Pure — no module state read.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChatIdAllowlist {
|
|
27
|
+
allowFrom: string[]
|
|
28
|
+
groups: Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveChatIdFallback(
|
|
32
|
+
rawChatId: string,
|
|
33
|
+
access: ChatIdAllowlist,
|
|
34
|
+
turnSessionChatId: string | undefined,
|
|
35
|
+
lastKnownChatId: string | undefined,
|
|
36
|
+
hasLiveTurn: boolean,
|
|
37
|
+
): { chatId: string; tier: 'raw' | 'active' | 'last-known' } {
|
|
38
|
+
if (access.allowFrom.includes(rawChatId) || rawChatId in access.groups) {
|
|
39
|
+
return { chatId: rawChatId, tier: 'raw' }
|
|
40
|
+
}
|
|
41
|
+
const fallbackChatId = turnSessionChatId ?? lastKnownChatId
|
|
42
|
+
if (fallbackChatId && (access.allowFrom.includes(fallbackChatId) || fallbackChatId in access.groups)) {
|
|
43
|
+
return { chatId: fallbackChatId, tier: hasLiveTurn ? 'active' : 'last-known' }
|
|
44
|
+
}
|
|
45
|
+
return { chatId: rawChatId, tier: 'raw' }
|
|
46
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// card, resolves the verdict back to hostd over IPC, and flips the card to
|
|
3
3
|
// a terminal state on finalize.
|
|
4
4
|
|
|
5
|
+
import { escapeMarkdown } from '../format.js';
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import type { IpcClient } from "./ipc-server.js";
|
|
7
8
|
import type {
|
|
@@ -180,10 +181,6 @@ const REASON_ELLIPSIS = "…";
|
|
|
180
181
|
*/
|
|
181
182
|
export const DIFF_SENTINEL = "\n[… diff continues, see attached file]";
|
|
182
183
|
|
|
183
|
-
function escapeMd(s: string): string {
|
|
184
|
-
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
184
|
function clipReason(reason: string): string {
|
|
188
185
|
if (reason.length <= REASON_MAX_CHARS) return reason;
|
|
189
186
|
return reason.slice(0, REASON_MAX_CHARS - REASON_ELLIPSIS.length) +
|
|
@@ -214,7 +211,7 @@ export function buildConfigApprovalCardBody(args: {
|
|
|
214
211
|
const render = (diff: string): string =>
|
|
215
212
|
`🛠 **Config edit proposed**\n` +
|
|
216
213
|
`Agent: \`${args.agentName}\`\n` +
|
|
217
|
-
`Reason: ${
|
|
214
|
+
`Reason: ${escapeMarkdown(safeReason)}\n\n` +
|
|
218
215
|
"```\n" + diff + "\n```";
|
|
219
216
|
|
|
220
217
|
return truncateRawToFit({
|
|
@@ -447,8 +444,8 @@ export function buildLiveNote(affectedAgents?: string[], fleetWide?: boolean): s
|
|
|
447
444
|
}
|
|
448
445
|
const agents = (affectedAgents ?? []).filter((a) => typeof a === "string" && a.length > 0);
|
|
449
446
|
if (agents.length === 0) return "";
|
|
450
|
-
const list = agents.map(
|
|
451
|
-
const cmds = agents.map((a) => `/restart ${
|
|
447
|
+
const list = agents.map(escapeMarkdown).join(", ");
|
|
448
|
+
const cmds = agents.map((a) => `/restart ${escapeMarkdown(a)}`).join(" · ");
|
|
452
449
|
return `\n\n🔄 Not live until restart — affects: **${list}**\n${cmds}`;
|
|
453
450
|
}
|
|
454
451
|
|
|
@@ -476,8 +473,8 @@ export async function handleRequestConfigFinalize(
|
|
|
476
473
|
msg.outcome === "applied" ? buildLiveNote(msg.affectedAgents, msg.fleetWide) : "";
|
|
477
474
|
const body =
|
|
478
475
|
msg.outcome === "applied"
|
|
479
|
-
? `✅ **Applied**${msg.detail ? `\n${
|
|
480
|
-
: `⚠️ **Reconcile failed; rolled back**${msg.detail ? `\n${
|
|
476
|
+
? `✅ **Applied**${msg.detail ? `\n${escapeMarkdown(msg.detail)}` : ""}${liveNote}`
|
|
477
|
+
: `⚠️ **Reconcile failed; rolled back**${msg.detail ? `\n${escapeMarkdown(msg.detail)}` : ""}`;
|
|
481
478
|
try {
|
|
482
479
|
// Finalize is terminal — strip the keyboard so the buttons are gone.
|
|
483
480
|
await deps.editCard({
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* single-shot "do this edit now" intent.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { escapeMarkdown } from '../format.js';
|
|
24
25
|
import { InlineKeyboard } from "grammy";
|
|
25
26
|
import type { DiffPreview } from "../../src/drive/diff-preview.js";
|
|
26
27
|
|
|
@@ -88,9 +89,9 @@ export function buildDiffPreviewCard(
|
|
|
88
89
|
// The 📍 + line-count rows are surfaced verbatim — they're
|
|
89
90
|
// wrapper-attested and the agent has no input into their content.
|
|
90
91
|
const bodyLines: string[] = [];
|
|
91
|
-
bodyLines.push(`**${
|
|
92
|
+
bodyLines.push(`**${escapeMarkdown(preview.title)}**`);
|
|
92
93
|
for (const line of preview.lines) {
|
|
93
|
-
bodyLines.push(
|
|
94
|
+
bodyLines.push(escapeMarkdown(line.text));
|
|
94
95
|
}
|
|
95
96
|
const text = bodyLines.join("\n");
|
|
96
97
|
|
|
@@ -161,7 +162,3 @@ export function buildDiffPreviewCard(
|
|
|
161
162
|
|
|
162
163
|
return { text, reply_markup: kb };
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
-
function escapeHtml(s: string): string {
|
|
166
|
-
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
167
|
-
}
|