switchroom 0.14.26 → 0.14.28
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/switchroom.js +20 -4
- package/dist/host-control/main.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/bridge/bridge.ts +15 -0
- package/telegram-plugin/card-format.ts +65 -0
- package/telegram-plugin/dist/bridge/bridge.js +14 -0
- package/telegram-plugin/dist/gateway/gateway.js +2201 -1748
- package/telegram-plugin/dist/server.js +14 -0
- package/telegram-plugin/gateway/gateway.ts +458 -13
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +1 -1
- package/telegram-plugin/history.ts +16 -4
- package/telegram-plugin/permission-title.ts +48 -0
- package/telegram-plugin/secret-detect/patterns.ts +8 -0
- package/telegram-plugin/secret-detect/redact.ts +76 -0
- package/telegram-plugin/tests/card-format.test.ts +96 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +80 -0
- package/telegram-plugin/tests/gateway-request-secret.test.ts +78 -0
- package/telegram-plugin/tests/history.test.ts +59 -0
- package/telegram-plugin/tests/permission-title.test.ts +68 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +35 -0
- package/telegram-plugin/tests/secret-detect-sanctum.test.ts +115 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +110 -51
- package/telegram-plugin/uat/assertions.ts +8 -6
- package/telegram-plugin/uat/feed-matcher.test.ts +14 -8
- package/telegram-plugin/uat/scenarios/jtbd-request-secret-dm.test.ts +101 -0
- package/telegram-plugin/uat/scenarios/jtbd-worker-activity-feed-dm.test.ts +17 -6
- package/telegram-plugin/worker-activity-feed.ts +84 -46
package/dist/cli/switchroom.js
CHANGED
|
@@ -49420,8 +49420,8 @@ var {
|
|
|
49420
49420
|
} = import__.default;
|
|
49421
49421
|
|
|
49422
49422
|
// src/build-info.ts
|
|
49423
|
-
var VERSION = "0.14.
|
|
49424
|
-
var COMMIT_SHA = "
|
|
49423
|
+
var VERSION = "0.14.28";
|
|
49424
|
+
var COMMIT_SHA = "cb64351f";
|
|
49425
49425
|
|
|
49426
49426
|
// src/cli/agent.ts
|
|
49427
49427
|
init_source();
|
|
@@ -50220,6 +50220,22 @@ a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
|
|
|
50220
50220
|
acknowledging, or *instead* of an update at a real milestone, is the
|
|
50221
50221
|
black box this exists to prevent.
|
|
50222
50222
|
|
|
50223
|
+
### Secrets \u2014 never ask for a chat paste
|
|
50224
|
+
|
|
50225
|
+
Never ask the user to paste a secret \u2014 an API key, token, password, or
|
|
50226
|
+
private key \u2014 as a normal chat message. It would persist in plaintext
|
|
50227
|
+
before anything can scrub it. Instead:
|
|
50228
|
+
|
|
50229
|
+
- Need a credential that isn't in the vault? Call
|
|
50230
|
+
\`request_secret(key, reason)\`. The operator gets a secure card, provides
|
|
50231
|
+
the value once, and it goes straight to the vault \u2014 you only ever see
|
|
50232
|
+
\`vault:<key>\`. After calling it, end your turn and wait for the reply.
|
|
50233
|
+
- The user *handed* you a value to keep? Use \`vault_request_save\`.
|
|
50234
|
+
- The key exists but you hit \`VAULT-BROKER-DENIED\`? Use \`vault_request_access\`.
|
|
50235
|
+
|
|
50236
|
+
Reference stored secrets only as \`vault:<key>\` \u2014 never echo a raw secret
|
|
50237
|
+
value back into chat.
|
|
50238
|
+
|
|
50223
50239
|
### Formatting \u2014 make it scannable
|
|
50224
50240
|
|
|
50225
50241
|
\`reply\` and \`stream_reply\` render Markdown as Telegram HTML for you, so
|
|
@@ -73962,6 +73978,7 @@ var ANCHORED_PATTERNS = [
|
|
|
73962
73978
|
{ rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
|
|
73963
73979
|
{ rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
|
|
73964
73980
|
{ rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
|
|
73981
|
+
{ rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
|
|
73965
73982
|
{ rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
|
|
73966
73983
|
{ rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
|
|
73967
73984
|
];
|
|
@@ -74216,7 +74233,7 @@ function dropOverlaps(hits) {
|
|
|
74216
74233
|
return out;
|
|
74217
74234
|
}
|
|
74218
74235
|
|
|
74219
|
-
//
|
|
74236
|
+
// telegram-plugin/secret-detect/redact.ts
|
|
74220
74237
|
var REDACTED_MARKER = "[REDACTED]";
|
|
74221
74238
|
function redact(text) {
|
|
74222
74239
|
if (!text || text.length === 0)
|
|
@@ -74239,7 +74256,6 @@ function redactedMarker(ruleId) {
|
|
|
74239
74256
|
}
|
|
74240
74257
|
return `[REDACTED:${trimmed}]`;
|
|
74241
74258
|
}
|
|
74242
|
-
|
|
74243
74259
|
// src/issues/store.ts
|
|
74244
74260
|
var ISSUES_FILE = "issues.jsonl";
|
|
74245
74261
|
var ISSUES_LOCK = "issues.lock";
|
|
@@ -19866,6 +19866,7 @@ var ANCHORED_PATTERNS = [
|
|
|
19866
19866
|
{ rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
|
|
19867
19867
|
{ rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
|
|
19868
19868
|
{ rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
|
|
19869
|
+
{ rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
|
|
19869
19870
|
{ rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
|
|
19870
19871
|
{ rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
|
|
19871
19872
|
];
|
|
@@ -20120,7 +20121,7 @@ function dropOverlaps(hits) {
|
|
|
20120
20121
|
return out;
|
|
20121
20122
|
}
|
|
20122
20123
|
|
|
20123
|
-
//
|
|
20124
|
+
// telegram-plugin/secret-detect/redact.ts
|
|
20124
20125
|
var REDACTED_MARKER = "[REDACTED]";
|
|
20125
20126
|
function redact(text) {
|
|
20126
20127
|
if (!text || text.length === 0)
|
|
@@ -20143,7 +20144,6 @@ function redactedMarker(ruleId) {
|
|
|
20143
20144
|
}
|
|
20144
20145
|
return `[REDACTED:${trimmed}]`;
|
|
20145
20146
|
}
|
|
20146
|
-
|
|
20147
20147
|
// src/cli/install-detect.ts
|
|
20148
20148
|
import * as fs from "node:fs";
|
|
20149
20149
|
import * as path from "node:path";
|
package/package.json
CHANGED
|
@@ -438,6 +438,21 @@ const TOOL_SCHEMAS = [
|
|
|
438
438
|
required: ['chat_id', 'key'],
|
|
439
439
|
},
|
|
440
440
|
},
|
|
441
|
+
{
|
|
442
|
+
name: 'request_secret',
|
|
443
|
+
description:
|
|
444
|
+
'Ask the operator to PROVIDE a secret you do not have, securely — NEVER ask the user to paste a token/key/password as a normal chat message. Use this when you need a credential that is not in the vault (a `vault:<key>` reference is missing/empty, or you know an upcoming task needs one you lack). Renders a Telegram card with [Provide securely] [Decline]; on tap, the operator sends the value once and the gateway DELETES their message instantly and writes it straight to the vault — the raw value is never echoed back to you. You receive only `vault:<key>`. This is the sibling of `vault_request_save` (use that when the user already handed YOU a value to store) and `vault_request_access` (use that when the key exists but you lack read access). After firing this tool, END YOUR TURN cleanly — a fresh inbound message arrives once the operator provides (or declines) the secret. Do NOT call this for keys you already have, and do NOT spam (one open request per key; the operator sees every card).',
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: 'object',
|
|
447
|
+
properties: {
|
|
448
|
+
chat_id: { type: 'string', description: 'Chat to render the card in (use the chat_id of the user message that triggered the workflow).' },
|
|
449
|
+
key: { type: 'string', description: 'Vault key to store the provided secret under. Use lowercase namespaced snake_case, e.g. `coolify/api-token`.' },
|
|
450
|
+
reason: { type: 'string', description: 'REQUIRED in practice — one-line human-readable rationale rendered on the card (e.g. "to trigger a redeploy on Coolify"). Omitting it makes the operator more likely to Decline.' },
|
|
451
|
+
message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
|
|
452
|
+
},
|
|
453
|
+
required: ['chat_id', 'key'],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
441
456
|
]
|
|
442
457
|
|
|
443
458
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
|
|
@@ -60,3 +60,68 @@ export function escapeHtml(s: string): string {
|
|
|
60
60
|
export function truncate(s: string, n: number): string {
|
|
61
61
|
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Strip Markdown markup from a single line, leaving plain prose.
|
|
66
|
+
*
|
|
67
|
+
* Worker narration is authored as Markdown — the model writes `**bold**`,
|
|
68
|
+
* `` `code` ``, `- bullets`, `# headings`. The status cards render Telegram
|
|
69
|
+
* HTML, which does NOT interpret Markdown, so an unstripped `**` shows up as
|
|
70
|
+
* two literal asterisks (the #94-class "half-done" look). Strip the markup
|
|
71
|
+
* here so the card reads as clean prose.
|
|
72
|
+
*
|
|
73
|
+
* Run this BEFORE `truncate` + `escapeHtml`: clean → measure → escape. (The
|
|
74
|
+
* stripper never touches `<`/`>`/`&`, so escaping stays the last step.)
|
|
75
|
+
*/
|
|
76
|
+
export function stripMarkdown(s: string): string {
|
|
77
|
+
let out = s;
|
|
78
|
+
// Inline + leftover code spans → bare text.
|
|
79
|
+
out = out.replace(/`+/g, '');
|
|
80
|
+
// Links / images: [text](url) and  → the label.
|
|
81
|
+
out = out.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1');
|
|
82
|
+
// Paired bold / emphasis runs (longest marker first).
|
|
83
|
+
out = out.replace(/\*\*(.+?)\*\*/g, '$1');
|
|
84
|
+
out = out.replace(/__(.+?)__/g, '$1');
|
|
85
|
+
out = out.replace(/\*(.+?)\*/g, '$1');
|
|
86
|
+
out = out.replace(/(?<![A-Za-z0-9])_(.+?)_(?![A-Za-z0-9])/g, '$1');
|
|
87
|
+
// Leading block markup: heading, blockquote, bullet, ordered item.
|
|
88
|
+
// `gm` so the markers are stripped on EVERY line, not just the string
|
|
89
|
+
// start — a multi-line summary like "Done.\n\n## Summary\n…" must not
|
|
90
|
+
// leak a raw `## Summary` when rendered as a single card step.
|
|
91
|
+
out = out.replace(/^\s{0,3}#{1,6}\s+/gm, '');
|
|
92
|
+
out = out.replace(/^\s{0,3}>\s?/gm, '');
|
|
93
|
+
out = out.replace(/^\s{0,3}[-*+]\s+/gm, '');
|
|
94
|
+
out = out.replace(/^\s{0,3}\d+[.)]\s+/gm, '');
|
|
95
|
+
// Residual unpaired bold markers (a lone `*` is left alone so `3 * 4`
|
|
96
|
+
// survives; only the doubled form is markup-by-construction).
|
|
97
|
+
out = out.replace(/\*\*/g, '');
|
|
98
|
+
return out.trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True for a whole-line horizontal rule: `---`, `___`, `***` (3+ of one). */
|
|
102
|
+
function isRuleLine(s: string): boolean {
|
|
103
|
+
return /^\s*([-_*])\1{2,}\s*$/.test(s);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clean a worker's multi-line result/narration into a single plain-text
|
|
108
|
+
* paragraph for a card's finished body. Drops fenced code blocks and
|
|
109
|
+
* horizontal rules, strips per-line Markdown, then space-joins what's left.
|
|
110
|
+
* Output is plain text — the caller still truncates + escapes before
|
|
111
|
+
* interpolating into HTML.
|
|
112
|
+
*/
|
|
113
|
+
export function cleanWorkerResultParagraph(s: string): string {
|
|
114
|
+
const kept: string[] = [];
|
|
115
|
+
let inFence = false;
|
|
116
|
+
for (const raw of s.split('\n')) {
|
|
117
|
+
if (/^\s*```/.test(raw)) {
|
|
118
|
+
inFence = !inFence;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (inFence) continue;
|
|
122
|
+
if (isRuleLine(raw)) continue;
|
|
123
|
+
const cleaned = stripMarkdown(raw);
|
|
124
|
+
if (cleaned.length > 0) kept.push(cleaned);
|
|
125
|
+
}
|
|
126
|
+
return kept.join(' ').replace(/\s+/g, ' ').trim();
|
|
127
|
+
}
|
|
@@ -24914,6 +24914,20 @@ var TOOL_SCHEMAS = [
|
|
|
24914
24914
|
},
|
|
24915
24915
|
required: ["chat_id", "key"]
|
|
24916
24916
|
}
|
|
24917
|
+
},
|
|
24918
|
+
{
|
|
24919
|
+
name: "request_secret",
|
|
24920
|
+
description: "Ask the operator to PROVIDE a secret you do not have, securely \u2014 NEVER ask the user to paste a token/key/password as a normal chat message. Use this when you need a credential that is not in the vault (a `vault:<key>` reference is missing/empty, or you know an upcoming task needs one you lack). Renders a Telegram card with [Provide securely] [Decline]; on tap, the operator sends the value once and the gateway DELETES their message instantly and writes it straight to the vault \u2014 the raw value is never echoed back to you. You receive only `vault:<key>`. This is the sibling of `vault_request_save` (use that when the user already handed YOU a value to store) and `vault_request_access` (use that when the key exists but you lack read access). After firing this tool, END YOUR TURN cleanly \u2014 a fresh inbound message arrives once the operator provides (or declines) the secret. Do NOT call this for keys you already have, and do NOT spam (one open request per key; the operator sees every card).",
|
|
24921
|
+
inputSchema: {
|
|
24922
|
+
type: "object",
|
|
24923
|
+
properties: {
|
|
24924
|
+
chat_id: { type: "string", description: "Chat to render the card in (use the chat_id of the user message that triggered the workflow)." },
|
|
24925
|
+
key: { type: "string", description: "Vault key to store the provided secret under. Use lowercase namespaced snake_case, e.g. `coolify/api-token`." },
|
|
24926
|
+
reason: { type: "string", description: 'REQUIRED in practice \u2014 one-line human-readable rationale rendered on the card (e.g. "to trigger a redeploy on Coolify"). Omitting it makes the operator more likely to Decline.' },
|
|
24927
|
+
message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message if not specified." }
|
|
24928
|
+
},
|
|
24929
|
+
required: ["chat_id", "key"]
|
|
24930
|
+
}
|
|
24917
24931
|
}
|
|
24918
24932
|
];
|
|
24919
24933
|
mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
|