switchroom 0.16.22 → 0.16.24
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/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2865 -48
- package/dist/cli/switchroom.js +338 -92
- package/dist/host-control/main.js +1 -1
- package/package.json +5 -5
- package/telegram-plugin/answer-stream.ts +13 -43
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/gateway/gateway.js +1670 -1892
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +101 -662
- package/telegram-plugin/gateway/approval-callback.ts +2 -3
- package/telegram-plugin/gateway/approval-card.test.ts +17 -4
- package/telegram-plugin/gateway/approval-card.ts +16 -6
- package/telegram-plugin/gateway/approvals-commands.ts +18 -24
- package/telegram-plugin/gateway/auth-command.ts +74 -74
- package/telegram-plugin/gateway/auth-line.ts +5 -15
- package/telegram-plugin/gateway/boot-card.ts +20 -22
- package/telegram-plugin/gateway/boot-version.ts +3 -2
- package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
- package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
- package/telegram-plugin/gateway/config-snapshot.ts +9 -9
- package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
- package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
- package/telegram-plugin/gateway/effort-command.ts +17 -17
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
- package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
- package/telegram-plugin/gateway/gateway.ts +751 -624
- package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
- package/telegram-plugin/gateway/inject-handler.ts +5 -5
- package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/linear-activity.ts +16 -14
- package/telegram-plugin/gateway/linear-setup.ts +1 -1
- package/telegram-plugin/gateway/model-command.ts +25 -25
- package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
- package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
- package/telegram-plugin/issues-card.ts +6 -7
- package/telegram-plugin/model-unavailable.ts +8 -12
- package/telegram-plugin/operator-events-history.ts +1 -1
- package/telegram-plugin/operator-events.ts +24 -28
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/pending-work-progress.ts +36 -36
- package/telegram-plugin/permission-title.ts +36 -20
- package/telegram-plugin/pty-partial-handler.ts +5 -13
- package/telegram-plugin/quota-check.ts +5 -5
- package/telegram-plugin/quota-watch.ts +13 -18
- package/telegram-plugin/recent-outbound-dedup.ts +5 -5
- package/telegram-plugin/registry/turns-schema.ts +44 -2
- package/telegram-plugin/retry-api-call.ts +15 -7
- package/telegram-plugin/rich-send.ts +57 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
- package/telegram-plugin/secret-detect/vault-error.ts +29 -22
- package/telegram-plugin/shared/bot-runtime.ts +23 -6
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +40 -97
- package/telegram-plugin/subagent-watcher.ts +2 -2
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
- package/telegram-plugin/tests/answer-stream.test.ts +54 -63
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
- package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bot-api.harness.ts +23 -1
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
- package/telegram-plugin/tests/card-format.test.ts +6 -4
- package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
- package/telegram-plugin/tests/credits-watch.test.ts +5 -5
- package/telegram-plugin/tests/fake-bot-api.ts +58 -4
- package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
- package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
- package/telegram-plugin/tests/issues-card.test.ts +15 -12
- package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
- package/telegram-plugin/tests/operator-events.test.ts +7 -9
- package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-title.test.ts +45 -41
- package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
- package/telegram-plugin/tests/quota-check.test.ts +3 -3
- package/telegram-plugin/tests/quota-watch.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
- package/telegram-plugin/tests/silence-poke.test.ts +75 -112
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
- package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
- package/telegram-plugin/tests/slot-banner.test.ts +9 -6
- package/telegram-plugin/tests/status-accent.test.ts +29 -32
- package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
- package/telegram-plugin/tests/stream-controller.test.ts +63 -52
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
- package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
- package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
- package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
- package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
- package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
- package/telegram-plugin/tests/welcome-text.test.ts +72 -65
- package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
- package/telegram-plugin/text-voice-scrub.ts +8 -11
- package/telegram-plugin/tool-activity-summary.ts +29 -29
- package/telegram-plugin/welcome-text.ts +82 -83
- package/telegram-plugin/worker-activity-feed.ts +2 -3
- package/telegram-plugin/html-sanitize.ts +0 -244
- package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
|
@@ -164,7 +164,7 @@ export async function handleApprovalCallback(
|
|
|
164
164
|
const newBody =
|
|
165
165
|
`${icon} ${displayMode}` +
|
|
166
166
|
(granted
|
|
167
|
-
? ` · /approvals revoke
|
|
167
|
+
? ` · /approvals revoke \`${decision_id}\``
|
|
168
168
|
: "");
|
|
169
169
|
|
|
170
170
|
const postTapKeyboard = granted && result.scope
|
|
@@ -172,8 +172,7 @@ export async function handleApprovalCallback(
|
|
|
172
172
|
: undefined;
|
|
173
173
|
|
|
174
174
|
try {
|
|
175
|
-
await ctx.editMessageText(newBody, {
|
|
176
|
-
parse_mode: "HTML",
|
|
175
|
+
await ctx.editMessageText({ markdown: newBody }, {
|
|
177
176
|
reply_markup: postTapKeyboard,
|
|
178
177
|
});
|
|
179
178
|
} catch {
|
|
@@ -44,15 +44,28 @@ describe("approval card", () => {
|
|
|
44
44
|
expect(datas).toContain("apv:a3f1b9c2a3f1b9c2a3f1b9c2a3f1b9c2:ttl:1h");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it("
|
|
47
|
+
it("passes < > & literally in agent and scope (markdown, #2669)", () => {
|
|
48
48
|
const card = buildApprovalCard({
|
|
49
49
|
request_id: "deadbeefdeadbeefdeadbeefdeadbeef",
|
|
50
50
|
agent: "<script>",
|
|
51
51
|
scope_humanized: "a&b",
|
|
52
52
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(card.text).toContain("
|
|
53
|
+
// < > & are literal in rich markdown. The agent is bolded (no specials
|
|
54
|
+
// here to escape) and the scope rides verbatim in a `code span`.
|
|
55
|
+
expect(card.text).toContain("**<script>**");
|
|
56
|
+
expect(card.text).toContain("`a&b`");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps a scope with underscores literal inside the code span (no \\_ leak, #2669)", () => {
|
|
60
|
+
// Regression: escaping inside the code span produced visible backslashes
|
|
61
|
+
// like `secret:OPENAI\_API\_KEY`. The scope must render verbatim.
|
|
62
|
+
const card = buildApprovalCard({
|
|
63
|
+
request_id: "deadbeefdeadbeefdeadbeefdeadbeef",
|
|
64
|
+
agent: "k",
|
|
65
|
+
scope_humanized: "secret:OPENAI_API_KEY",
|
|
66
|
+
});
|
|
67
|
+
expect(card.text).toContain("`secret:OPENAI_API_KEY`");
|
|
68
|
+
expect(card.text).not.toContain("\\_");
|
|
56
69
|
});
|
|
57
70
|
});
|
|
58
71
|
|
|
@@ -37,8 +37,12 @@ export interface BuiltApprovalCard {
|
|
|
37
37
|
*/
|
|
38
38
|
export function buildApprovalCard(opts: ApprovalCardOptions): BuiltApprovalCard {
|
|
39
39
|
const lines: string[] = [];
|
|
40
|
-
lines.push(`🔐
|
|
41
|
-
|
|
40
|
+
lines.push(`🔐 **${escapeHtml(opts.agent)}** wants approval`);
|
|
41
|
+
// The scope rides in a `code span` — content there is LITERAL, so it must
|
|
42
|
+
// NOT be markdown-escaped (escaping would leak visible backslashes, e.g.
|
|
43
|
+
// `secret:OPENAI\_API\_KEY`). Only an embedded backtick could break out of
|
|
44
|
+
// the span, so defuse that one char with a zero-width space (#2669).
|
|
45
|
+
lines.push(`\`${codeSpanSafe(opts.scope_humanized)}\``);
|
|
42
46
|
if (opts.why && opts.why.trim().length > 0) {
|
|
43
47
|
lines.push("");
|
|
44
48
|
lines.push(escapeHtml(opts.why.trim()));
|
|
@@ -120,8 +124,14 @@ export function ttlMsFromToken(token: string): number | null {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
function escapeHtml(s: string): string {
|
|
123
|
-
return s
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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, "`");
|
|
127
137
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Bot, Context } from "grammy";
|
|
17
|
+
import { richMessage } from "../rich-send.js";
|
|
17
18
|
import {
|
|
18
19
|
approvalList,
|
|
19
20
|
approvalRevoke,
|
|
@@ -52,12 +53,11 @@ export function registerApprovalsCommands(
|
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
if (decisions.length === 0) {
|
|
55
|
-
await ctx.
|
|
56
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
56
57
|
agentFilter
|
|
57
|
-
? `No active approvals for
|
|
58
|
+
? `No active approvals for \`${agentFilter}\`.`
|
|
58
59
|
: "No active approvals.",
|
|
59
|
-
|
|
60
|
-
);
|
|
60
|
+
));
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
// Top-level summary by agent, mirroring the GitHub-style permissions UI
|
|
@@ -65,7 +65,7 @@ export function registerApprovalsCommands(
|
|
|
65
65
|
const byAgent = new Map<string, number>();
|
|
66
66
|
for (const d of decisions) byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
|
|
67
67
|
const summary = Array.from(byAgent.entries())
|
|
68
|
-
.map(([a, n]) => `•
|
|
68
|
+
.map(([a, n]) => `• **${escapeHtml(a)}**: ${n}`)
|
|
69
69
|
.join("\n");
|
|
70
70
|
const detail = decisions
|
|
71
71
|
.slice(0, 20)
|
|
@@ -75,17 +75,15 @@ export function registerApprovalsCommands(
|
|
|
75
75
|
? "always"
|
|
76
76
|
: `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
|
|
77
77
|
return (
|
|
78
|
-
|
|
78
|
+
`\`${d.id.slice(0, 8)}\` ` +
|
|
79
79
|
`${escapeHtml(d.agent_unit)} → ` +
|
|
80
|
-
|
|
80
|
+
`\`${d.scope}\` ` +
|
|
81
81
|
`(${escapeHtml(d.action)}, ${ttl}) ` +
|
|
82
82
|
`· /approvals revoke ${escapeHtml(d.id)}`
|
|
83
83
|
);
|
|
84
84
|
})
|
|
85
85
|
.join("\n");
|
|
86
|
-
await ctx.
|
|
87
|
-
parse_mode: "HTML",
|
|
88
|
-
});
|
|
86
|
+
await ctx.replyWithRichMessage(richMessage(`**Active approvals**\n\n${summary}\n\n${detail}`));
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
91
89
|
|
|
@@ -93,9 +91,7 @@ export function registerApprovalsCommands(
|
|
|
93
91
|
if (sub === "revoke") {
|
|
94
92
|
const id = args[1];
|
|
95
93
|
if (!id) {
|
|
96
|
-
await ctx.
|
|
97
|
-
parse_mode: "HTML",
|
|
98
|
-
});
|
|
94
|
+
await ctx.replyWithRichMessage(richMessage("Usage: `/approvals revoke <id>`"));
|
|
99
95
|
return;
|
|
100
96
|
}
|
|
101
97
|
const actor = ctx.from?.id?.toString() ?? "unknown";
|
|
@@ -104,23 +100,21 @@ export function registerApprovalsCommands(
|
|
|
104
100
|
await ctx.reply("Approval kernel unreachable.");
|
|
105
101
|
return;
|
|
106
102
|
}
|
|
107
|
-
await ctx.
|
|
108
|
-
ok ? `Revoked
|
|
109
|
-
|
|
110
|
-
);
|
|
103
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
104
|
+
ok ? `Revoked \`${id}\`.` : `No such active decision \`${id}\`.`,
|
|
105
|
+
));
|
|
111
106
|
return;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
109
|
// /approvals add and /approvals stats — TODO Phase 1.5
|
|
115
|
-
await ctx.
|
|
116
|
-
`Unknown subcommand
|
|
117
|
-
`Use
|
|
118
|
-
`(
|
|
119
|
-
|
|
120
|
-
);
|
|
110
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
111
|
+
`Unknown subcommand \`${sub}\`. ` +
|
|
112
|
+
`Use \`/approvals list\` or \`/approvals revoke <id>\`. ` +
|
|
113
|
+
`(\`add\` and \`stats\` are coming in a follow-up.)`,
|
|
114
|
+
));
|
|
121
115
|
});
|
|
122
116
|
}
|
|
123
117
|
|
|
124
118
|
function escapeHtml(s: string): string {
|
|
125
|
-
return s.replace(
|
|
119
|
+
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
126
120
|
}
|
|
@@ -95,7 +95,7 @@ export function validateAuthAddLabel(label: string): string | null {
|
|
|
95
95
|
return 'Label cannot contain path separators.'
|
|
96
96
|
}
|
|
97
97
|
if (!LABEL_RE.test(label)) {
|
|
98
|
-
return 'Label must match
|
|
98
|
+
return 'Label must match \`[A-Za-z0-9._@+-]+\` (letters, digits, dot, underscore, dash, @, +).'
|
|
99
99
|
}
|
|
100
100
|
return null
|
|
101
101
|
}
|
|
@@ -158,7 +158,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
158
158
|
if (tail.length > 0) {
|
|
159
159
|
return {
|
|
160
160
|
kind: 'help',
|
|
161
|
-
reason: `Unknown
|
|
161
|
+
reason: `Unknown \`rm\` modifier: \`${escapeHtml(tail)}\`. Use \`/auth rm <label> confirm\` to confirm.`,
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
return { kind: 'rm-prompt', label }
|
|
@@ -180,7 +180,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
180
180
|
if (sub !== 'override') {
|
|
181
181
|
return {
|
|
182
182
|
kind: 'help',
|
|
183
|
-
reason: `Unknown
|
|
183
|
+
reason: `Unknown \`agent\` subcommand: \`${escapeHtml(sub || '(none)')}\`. Try \`/auth agent override <agent> <label|clear>\`.`,
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
const agent = parts[2]
|
|
@@ -188,7 +188,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
188
188
|
if (!agent || !target) {
|
|
189
189
|
return {
|
|
190
190
|
kind: 'help',
|
|
191
|
-
reason: 'Usage: /auth agent override
|
|
191
|
+
reason: 'Usage: /auth agent override <agent> <label|clear>',
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
if (target.toLowerCase() === 'clear') {
|
|
@@ -201,7 +201,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
201
201
|
case 'help':
|
|
202
202
|
return { kind: 'help' }
|
|
203
203
|
default:
|
|
204
|
-
return { kind: 'help', reason: `Unknown verb:
|
|
204
|
+
return { kind: 'help', reason: `Unknown verb: \`${escapeHtml(verb)}\`` }
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -351,19 +351,19 @@ export async function handleAuthCommand(
|
|
|
351
351
|
const reason = parsed.reason ? `${parsed.reason}\n\n` : ''
|
|
352
352
|
return {
|
|
353
353
|
text:
|
|
354
|
-
`${reason}
|
|
355
|
-
`
|
|
356
|
-
`
|
|
357
|
-
`
|
|
358
|
-
`
|
|
359
|
-
`
|
|
360
|
-
`
|
|
361
|
-
`
|
|
362
|
-
`
|
|
363
|
-
`
|
|
364
|
-
`
|
|
365
|
-
`
|
|
366
|
-
`
|
|
354
|
+
`${reason}**/auth** — verbs (mirror of \`switchroom auth\`):\n` +
|
|
355
|
+
` \`/auth\` — show fleet snapshot (alias of \`show\`)\n` +
|
|
356
|
+
` \`/auth show\` — show fleet snapshot\n` +
|
|
357
|
+
` \`/auth show <agent>\` — show one agent's effective account + mirror state\n` +
|
|
358
|
+
` \`/auth list\` — alias of \`/auth show\`\n` +
|
|
359
|
+
` \`/auth use <label>\` — admin: swap the fleet to <label>\n` +
|
|
360
|
+
` \`/auth rotate\` — admin: cycle to next non-exhausted fallback\n` +
|
|
361
|
+
` \`/auth add <label>\` — admin: OAuth-add a new account from chat\n` +
|
|
362
|
+
` \`/auth cancel\` — abort an \`/auth add\` in progress\n` +
|
|
363
|
+
` \`/auth rm <label>\` — admin: remove an account (two-step confirm)\n` +
|
|
364
|
+
` \`/auth refresh [<label>]\` — admin: force a refresh tick\n` +
|
|
365
|
+
` \`/auth agent override <agent> <label|clear>\` — admin: per-agent account override\n` +
|
|
366
|
+
` \`/auth help\` — this list`,
|
|
367
367
|
html: true,
|
|
368
368
|
}
|
|
369
369
|
}
|
|
@@ -417,7 +417,7 @@ export async function handleAuthCommand(
|
|
|
417
417
|
}
|
|
418
418
|
} catch (err) {
|
|
419
419
|
return {
|
|
420
|
-
text:
|
|
420
|
+
text: `**/auth show failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
421
421
|
html: true,
|
|
422
422
|
}
|
|
423
423
|
}
|
|
@@ -432,15 +432,15 @@ export async function handleAuthCommand(
|
|
|
432
432
|
if (!agent) {
|
|
433
433
|
return {
|
|
434
434
|
text:
|
|
435
|
-
|
|
436
|
-
`Run
|
|
435
|
+
`**/auth show:** no agent named \`${escapeHtml(agentName)}\` in broker view.\n` +
|
|
436
|
+
`Run \`/auth show\` for the fleet snapshot.`,
|
|
437
437
|
html: true,
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
return { text: renderAgentDetail(state, agent), html: true }
|
|
441
441
|
} catch (err) {
|
|
442
442
|
return {
|
|
443
|
-
text:
|
|
443
|
+
text: `**/auth show failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
444
444
|
html: true,
|
|
445
445
|
}
|
|
446
446
|
}
|
|
@@ -450,10 +450,10 @@ export async function handleAuthCommand(
|
|
|
450
450
|
if (!isAdmin(ctx)) {
|
|
451
451
|
return {
|
|
452
452
|
text:
|
|
453
|
-
|
|
454
|
-
`Set
|
|
455
|
-
`(the same flag that gates
|
|
456
|
-
|
|
453
|
+
`**Not authorized.** \`/auth ${parsed.kind}\` is admin-only.\n` +
|
|
454
|
+
`Set \`admin: true\` on this agent in switchroom.yaml to unlock ` +
|
|
455
|
+
`(the same flag that gates \`/agents\`, \`/restart\`, ` +
|
|
456
|
+
`\`/update\` etc.).`,
|
|
457
457
|
html: true,
|
|
458
458
|
}
|
|
459
459
|
}
|
|
@@ -466,7 +466,7 @@ export async function handleAuthCommand(
|
|
|
466
466
|
if (parsed.kind === 'add' || parsed.kind === 'cancel') {
|
|
467
467
|
return {
|
|
468
468
|
text:
|
|
469
|
-
|
|
469
|
+
`**/auth ${parsed.kind} not routed.** Internal error — gateway should dispatch this verb directly. Report this.`,
|
|
470
470
|
html: true,
|
|
471
471
|
}
|
|
472
472
|
}
|
|
@@ -476,13 +476,13 @@ export async function handleAuthCommand(
|
|
|
476
476
|
const result = await ctx.client.setActive(parsed.label)
|
|
477
477
|
return {
|
|
478
478
|
text:
|
|
479
|
-
|
|
479
|
+
`**Active account →** \`${escapeHtml(result.active)}\`\n` +
|
|
480
480
|
`Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
|
|
481
481
|
html: true,
|
|
482
482
|
}
|
|
483
483
|
} catch (err) {
|
|
484
484
|
return {
|
|
485
|
-
text:
|
|
485
|
+
text: `**/auth use failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
486
486
|
html: true,
|
|
487
487
|
}
|
|
488
488
|
}
|
|
@@ -495,8 +495,8 @@ export async function handleAuthCommand(
|
|
|
495
495
|
if (!nextLabel) {
|
|
496
496
|
return {
|
|
497
497
|
text:
|
|
498
|
-
|
|
499
|
-
`Either every account in
|
|
498
|
+
`**/auth rotate** — no eligible target.\n` +
|
|
499
|
+
`Either every account in \`fallback_order\` is exhausted, ` +
|
|
500
500
|
`or no fallback order is configured.`,
|
|
501
501
|
html: true,
|
|
502
502
|
}
|
|
@@ -504,13 +504,13 @@ export async function handleAuthCommand(
|
|
|
504
504
|
const result = await ctx.client.setActive(nextLabel)
|
|
505
505
|
return {
|
|
506
506
|
text:
|
|
507
|
-
|
|
507
|
+
`**Rotated:** active → \`${escapeHtml(result.active)}\`\n` +
|
|
508
508
|
`Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
|
|
509
509
|
html: true,
|
|
510
510
|
}
|
|
511
511
|
} catch (err) {
|
|
512
512
|
return {
|
|
513
|
-
text:
|
|
513
|
+
text: `**/auth rotate failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
514
514
|
html: true,
|
|
515
515
|
}
|
|
516
516
|
}
|
|
@@ -525,7 +525,7 @@ export async function handleAuthCommand(
|
|
|
525
525
|
state = await ctx.client.listState()
|
|
526
526
|
} catch (err) {
|
|
527
527
|
return {
|
|
528
|
-
text:
|
|
528
|
+
text: `**/auth rm failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
529
529
|
html: true,
|
|
530
530
|
}
|
|
531
531
|
}
|
|
@@ -533,16 +533,16 @@ export async function handleAuthCommand(
|
|
|
533
533
|
if (!exists) {
|
|
534
534
|
return {
|
|
535
535
|
text:
|
|
536
|
-
|
|
537
|
-
`Run
|
|
536
|
+
`**/auth rm:** no account named \`${escapeHtml(parsed.label)}\`. ` +
|
|
537
|
+
`Run \`/auth show\` for the current list.`,
|
|
538
538
|
html: true,
|
|
539
539
|
}
|
|
540
540
|
}
|
|
541
541
|
if (state.active === parsed.label) {
|
|
542
542
|
return {
|
|
543
543
|
text:
|
|
544
|
-
|
|
545
|
-
`Switch with
|
|
544
|
+
`**/auth rm refused.** \`${escapeHtml(parsed.label)}\` is the fleet active. ` +
|
|
545
|
+
`Switch with \`/auth use <other>\` or \`/auth rotate\` first.`,
|
|
546
546
|
html: true,
|
|
547
547
|
}
|
|
548
548
|
}
|
|
@@ -560,9 +560,9 @@ export async function handleAuthCommand(
|
|
|
560
560
|
}
|
|
561
561
|
return {
|
|
562
562
|
text:
|
|
563
|
-
|
|
564
|
-
`The fleet active is unchanged. Any agent override pointing at
|
|
565
|
-
`Send
|
|
563
|
+
`**⚠ /auth rm** — about to remove \`${escapeHtml(parsed.label)}\` from the broker.\n` +
|
|
564
|
+
`The fleet active is unchanged. Any agent override pointing at \`${escapeHtml(parsed.label)}\` will stop working.\n\n` +
|
|
565
|
+
`Send \`/auth rm ${escapeHtml(parsed.label)} confirm\` within ${Math.round(
|
|
566
566
|
AUTH_RM_CONFIRM_TTL_MS / 1000,
|
|
567
567
|
)}s to proceed.`,
|
|
568
568
|
html: true,
|
|
@@ -578,8 +578,8 @@ export async function handleAuthCommand(
|
|
|
578
578
|
}
|
|
579
579
|
return {
|
|
580
580
|
text:
|
|
581
|
-
|
|
582
|
-
`Send
|
|
581
|
+
`**/auth rm:** no pending confirm for \`${escapeHtml(parsed.label)}\` (expired or not started). ` +
|
|
582
|
+
`Send \`/auth rm ${escapeHtml(parsed.label)}\` first.`,
|
|
583
583
|
html: true,
|
|
584
584
|
}
|
|
585
585
|
}
|
|
@@ -589,12 +589,12 @@ export async function handleAuthCommand(
|
|
|
589
589
|
try {
|
|
590
590
|
const data = await ctx.client.rmAccount(parsed.label)
|
|
591
591
|
return {
|
|
592
|
-
text:
|
|
592
|
+
text: `**Removed** \`${escapeHtml(data.label)}\` from the broker.`,
|
|
593
593
|
html: true,
|
|
594
594
|
}
|
|
595
595
|
} catch (err) {
|
|
596
596
|
return {
|
|
597
|
-
text:
|
|
597
|
+
text: `**/auth rm failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
598
598
|
html: true,
|
|
599
599
|
}
|
|
600
600
|
}
|
|
@@ -609,12 +609,12 @@ export async function handleAuthCommand(
|
|
|
609
609
|
if (parsed.label && targets.length === 0) {
|
|
610
610
|
return {
|
|
611
611
|
text:
|
|
612
|
-
|
|
612
|
+
`**/auth refresh:** no account named \`${escapeHtml(parsed.label)}\`.`,
|
|
613
613
|
html: true,
|
|
614
614
|
}
|
|
615
615
|
}
|
|
616
616
|
if (targets.length === 0) {
|
|
617
|
-
return { text:
|
|
617
|
+
return { text: `**/auth refresh:** no accounts to refresh.`, html: true }
|
|
618
618
|
}
|
|
619
619
|
const oldByLabel = new Map(state.accounts.map((a) => [a.label, a.expiresAt]))
|
|
620
620
|
const rows: string[][] = [['ACCOUNT', 'OLD EXPIRY', 'NEW EXPIRY']]
|
|
@@ -635,18 +635,18 @@ export async function handleAuthCommand(
|
|
|
635
635
|
}
|
|
636
636
|
const head =
|
|
637
637
|
targets.length === 1
|
|
638
|
-
?
|
|
639
|
-
:
|
|
638
|
+
? `**Refreshed** \`${escapeHtml(targets[0]!)}\``
|
|
639
|
+
: `**Refreshed** ${rows.length - 1}/${targets.length} account${targets.length === 1 ? '' : 's'}`
|
|
640
640
|
const table = rows.length > 1
|
|
641
|
-
?
|
|
641
|
+
? "\n```\n" + alignTable(rows) + "\n```"
|
|
642
642
|
: ''
|
|
643
643
|
const failBlock = failures.length > 0
|
|
644
|
-
? `\n
|
|
644
|
+
? `\n**Failures:**\n${failures.map((f) => ` ${f}`).join('\n')}`
|
|
645
645
|
: ''
|
|
646
646
|
return { text: head + table + failBlock, html: true }
|
|
647
647
|
} catch (err) {
|
|
648
648
|
return {
|
|
649
|
-
text:
|
|
649
|
+
text: `**/auth refresh failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
650
650
|
html: true,
|
|
651
651
|
}
|
|
652
652
|
}
|
|
@@ -657,13 +657,13 @@ export async function handleAuthCommand(
|
|
|
657
657
|
const data = await ctx.client.setOverride(parsed.agent, parsed.label)
|
|
658
658
|
return {
|
|
659
659
|
text:
|
|
660
|
-
|
|
661
|
-
|
|
660
|
+
`**Override set.** \`${escapeHtml(data.agent)}\` is now pinned to ` +
|
|
661
|
+
`\`${escapeHtml(data.account ?? parsed.label)}\`.`,
|
|
662
662
|
html: true,
|
|
663
663
|
}
|
|
664
664
|
} catch (err) {
|
|
665
665
|
return {
|
|
666
|
-
text:
|
|
666
|
+
text: `**/auth agent override failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
667
667
|
html: true,
|
|
668
668
|
}
|
|
669
669
|
}
|
|
@@ -674,13 +674,13 @@ export async function handleAuthCommand(
|
|
|
674
674
|
const data = await ctx.client.setOverride(parsed.agent, null)
|
|
675
675
|
return {
|
|
676
676
|
text:
|
|
677
|
-
|
|
677
|
+
`**Override cleared** on \`${escapeHtml(data.agent)}\` ` +
|
|
678
678
|
`— back to fleet active.`,
|
|
679
679
|
html: true,
|
|
680
680
|
}
|
|
681
681
|
} catch (err) {
|
|
682
682
|
return {
|
|
683
|
-
text:
|
|
683
|
+
text: `**/auth agent override failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
|
|
684
684
|
html: true,
|
|
685
685
|
}
|
|
686
686
|
}
|
|
@@ -690,7 +690,7 @@ export async function handleAuthCommand(
|
|
|
690
690
|
const _exhaustive: never = parsed
|
|
691
691
|
void _exhaustive
|
|
692
692
|
return {
|
|
693
|
-
text:
|
|
693
|
+
text: `**/auth:** unhandled verb. Report this.`,
|
|
694
694
|
html: true,
|
|
695
695
|
}
|
|
696
696
|
}
|
|
@@ -800,37 +800,37 @@ export function renderShowText(
|
|
|
800
800
|
}),
|
|
801
801
|
)
|
|
802
802
|
} else {
|
|
803
|
-
lines.push('
|
|
803
|
+
lines.push('**Auth — fleet snapshot**')
|
|
804
804
|
if (state.accounts.length > 0) {
|
|
805
805
|
lines.push('')
|
|
806
|
-
lines.push('
|
|
807
|
-
lines.push('
|
|
806
|
+
lines.push('**Accounts**')
|
|
807
|
+
lines.push('```')
|
|
808
808
|
lines.push(formatAccountsTable(state, now, opts.demo ?? false))
|
|
809
|
-
lines.push('
|
|
809
|
+
lines.push('```')
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
812
|
|
|
813
813
|
// Agents table
|
|
814
814
|
if (state.agents.length > 0) {
|
|
815
|
-
lines.push('
|
|
816
|
-
lines.push('
|
|
815
|
+
lines.push('**Agents**')
|
|
816
|
+
lines.push('```')
|
|
817
817
|
lines.push(formatAgentsTable(state, opts.demo ?? false))
|
|
818
|
-
lines.push('
|
|
818
|
+
lines.push('```')
|
|
819
819
|
}
|
|
820
820
|
|
|
821
821
|
// Consumers table — only when there are any (typical case: hindsight).
|
|
822
822
|
if (state.consumers.length > 0) {
|
|
823
|
-
lines.push('
|
|
824
|
-
lines.push('
|
|
823
|
+
lines.push('**Consumers**')
|
|
824
|
+
lines.push('```')
|
|
825
825
|
lines.push(formatConsumersTable(state, now, opts.demo ?? false))
|
|
826
|
-
lines.push('
|
|
826
|
+
lines.push('```')
|
|
827
827
|
}
|
|
828
828
|
|
|
829
829
|
// Discovery hint — operators on a quota-walled fleet need to know
|
|
830
830
|
// `/auth add` exists so they can add a fresh account without an
|
|
831
831
|
// LLM in the loop. Keep it short; the help text has the full menu.
|
|
832
832
|
lines.push(
|
|
833
|
-
'
|
|
833
|
+
'_Add a new Anthropic account: \`/auth add <label>\` (admin)_',
|
|
834
834
|
)
|
|
835
835
|
|
|
836
836
|
return lines.join('\n')
|
|
@@ -909,10 +909,10 @@ export function renderAgentDetail(
|
|
|
909
909
|
now: number = Date.now(),
|
|
910
910
|
): string {
|
|
911
911
|
const lines: string[] = []
|
|
912
|
-
lines.push(
|
|
912
|
+
lines.push(`**${escapeHtml(agent.name)}**`)
|
|
913
913
|
const source = agent.override ? 'override' : 'fleet-active'
|
|
914
914
|
lines.push(
|
|
915
|
-
`Active account:
|
|
915
|
+
`Active account: \`${escapeHtml(agent.account)}\` (${source})`,
|
|
916
916
|
)
|
|
917
917
|
const acct = state.accounts.find((a) => a.label === agent.account)
|
|
918
918
|
if (acct) {
|
|
@@ -928,11 +928,11 @@ export function renderAgentDetail(
|
|
|
928
928
|
acct.exhausted_until != null && acct.exhausted_until > now
|
|
929
929
|
? formatRelativeMs(acct.exhausted_until - now)
|
|
930
930
|
: '—'
|
|
931
|
-
lines.push(
|
|
931
|
+
lines.push(`_Quota: exhausted · resets in ${resetRel}_`)
|
|
932
932
|
}
|
|
933
933
|
if (typeof acct.threshold_violations === 'number' && acct.threshold_violations > 0) {
|
|
934
934
|
lines.push(
|
|
935
|
-
|
|
935
|
+
`_Threshold violations: ${acct.threshold_violations} — claude refreshed under the broker's feet_`,
|
|
936
936
|
)
|
|
937
937
|
}
|
|
938
938
|
}
|
|
@@ -956,7 +956,7 @@ function formatConsumersTable(state: ListStateData, now: number, demo = false):
|
|
|
956
956
|
// ─── Plain-text helpers ────────────────────────────────────────────────────
|
|
957
957
|
|
|
958
958
|
function escapeHtml(s: string): string {
|
|
959
|
-
return s.replace(
|
|
959
|
+
return s.replace(/([\\`*_~=\[\]|])/g, '\\$1')
|
|
960
960
|
}
|
|
961
961
|
|
|
962
962
|
function formatRelativeMs(ms: number): string {
|
|
@@ -22,16 +22,6 @@ import type { ListStateData, AccountState } from '../../src/auth/broker/client.j
|
|
|
22
22
|
|
|
23
23
|
export type { ListStateData, AccountState }
|
|
24
24
|
|
|
25
|
-
// Local HTML-escape (mirrors the helper formerly co-located in
|
|
26
|
-
// auth-dashboard.ts so we keep the same escaping discipline without
|
|
27
|
-
// pulling in a heavier util).
|
|
28
|
-
function escapeHtml(s: string): string {
|
|
29
|
-
return s
|
|
30
|
-
.replace(/&/g, '&')
|
|
31
|
-
.replace(/</g, '<')
|
|
32
|
-
.replace(/>/g, '>')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
/** Format a duration in ms as a short relative string ("1h 22m", "12s"). */
|
|
36
26
|
function formatRelativeMs(ms: number): string {
|
|
37
27
|
if (ms <= 0) return '0s'
|
|
@@ -55,9 +45,9 @@ export function formatAuthQuotaLine(acc: AccountState, now: number = Date.now())
|
|
|
55
45
|
if (acc.exhausted) {
|
|
56
46
|
const until = acc.exhausted_until
|
|
57
47
|
if (until != null && until > now) {
|
|
58
|
-
return
|
|
48
|
+
return `_exhausted · resets in ${formatRelativeMs(until - now)}_`
|
|
59
49
|
}
|
|
60
|
-
return
|
|
50
|
+
return `_exhausted_`
|
|
61
51
|
}
|
|
62
52
|
return null
|
|
63
53
|
}
|
|
@@ -110,14 +100,14 @@ export function renderAuthLine(
|
|
|
110
100
|
|
|
111
101
|
const byLabel = new Map(state.accounts.map((a) => [a.label, a]))
|
|
112
102
|
const rows: string[] = []
|
|
113
|
-
rows.push(
|
|
103
|
+
rows.push(`**Accounts (${state.accounts.length})**`)
|
|
114
104
|
for (const label of order) {
|
|
115
105
|
const acc = byLabel.get(label)
|
|
116
106
|
if (!acc) continue
|
|
117
107
|
const marker = label === activeLabel ? '▶' : '↳'
|
|
118
|
-
const
|
|
108
|
+
const labelMd = `\`${acc.label}\``
|
|
119
109
|
const quotaLine = formatAuthQuotaLine(acc, now)
|
|
120
|
-
rows.push(quotaLine ? `${marker} ${
|
|
110
|
+
rows.push(quotaLine ? `${marker} ${labelMd} ${quotaLine}` : `${marker} ${labelMd}`)
|
|
121
111
|
}
|
|
122
112
|
return rows
|
|
123
113
|
}
|