cli-jaw 2.2.9 → 2.2.10
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/README.md +4 -4
- package/dist/bin/commands/doctor.js +66 -1
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/init.js +107 -8
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/lib/upload.js +5 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/alert-escalation.js +1 -1
- package/dist/src/agent/alert-escalation.js.map +1 -1
- package/dist/src/agent/codex-app-catalog.js +57 -0
- package/dist/src/agent/codex-app-catalog.js.map +1 -0
- package/dist/src/agent/codex-app-client.js +89 -7
- package/dist/src/agent/codex-app-client.js.map +1 -1
- package/dist/src/agent/pi-rpc-verdict.js +22 -0
- package/dist/src/agent/pi-rpc-verdict.js.map +1 -0
- package/dist/src/agent/pi-runtime.js +240 -0
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/runtime-pool.js +445 -0
- package/dist/src/agent/runtime-pool.js.map +1 -0
- package/dist/src/agent/spawn.js +395 -240
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/command-context.js +5 -2
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/cli/commands.js +43 -39
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +15 -7
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/handlers-search.js +1 -1
- package/dist/src/cli/handlers-search.js.map +1 -1
- package/dist/src/cli/handlers-skill-invoke.js +1 -1
- package/dist/src/cli/handlers-skill-invoke.js.map +1 -1
- package/dist/src/cli/handlers-workflows.js +1 -1
- package/dist/src/cli/handlers-workflows.js.map +1 -1
- package/dist/src/cli/handlers.js +1 -1
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/opencodex-models.js +40 -7
- package/dist/src/cli/opencodex-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +6 -5
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/command-contract/catalog.js +4 -0
- package/dist/src/command-contract/catalog.js.map +1 -1
- package/dist/src/core/config.js +69 -4
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/runtime-settings.js +17 -0
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-merge.js +1 -1
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/discord/bot.js +29 -14
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/commands.js +5 -4
- package/dist/src/discord/commands.js.map +1 -1
- package/dist/src/discord/discord-file.js +8 -2
- package/dist/src/discord/discord-file.js.map +1 -1
- package/dist/src/discord/forwarder.js +17 -19
- package/dist/src/discord/forwarder.js.map +1 -1
- package/dist/src/discord/send-only-client.js +9 -1
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/dashboard-service.js +38 -1
- package/dist/src/manager/dashboard-service.js.map +1 -1
- package/dist/src/manager/telegram-hub/hub-bot.js +47 -29
- package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
- package/dist/src/messaging/channel-health.js +32 -0
- package/dist/src/messaging/channel-health.js.map +1 -1
- package/dist/src/messaging/chunk.js +376 -0
- package/dist/src/messaging/chunk.js.map +1 -0
- package/dist/src/messaging/dedupe.js +104 -0
- package/dist/src/messaging/dedupe.js.map +1 -0
- package/dist/src/messaging/fold.js +210 -0
- package/dist/src/messaging/fold.js.map +1 -0
- package/dist/src/messaging/redact.js +577 -0
- package/dist/src/messaging/redact.js.map +1 -0
- package/dist/src/messaging/retry.js +76 -0
- package/dist/src/messaging/retry.js.map +1 -0
- package/dist/src/messaging/runtime.js +6 -3
- package/dist/src/messaging/runtime.js.map +1 -1
- package/dist/src/messaging/send.js +45 -15
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/slack-target.js +50 -0
- package/dist/src/messaging/slack-target.js.map +1 -0
- package/dist/src/messaging/types.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +4 -4
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/routes/command.js +11 -7
- package/dist/src/routes/command.js.map +1 -1
- package/dist/src/routes/messaging.js +28 -12
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/settings.js +22 -6
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/slack/api.js +116 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/bot.js +262 -0
- package/dist/src/slack/bot.js.map +1 -0
- package/dist/src/slack/commands.js +98 -0
- package/dist/src/slack/commands.js.map +1 -0
- package/dist/src/slack/events.js +116 -0
- package/dist/src/slack/events.js.map +1 -0
- package/dist/src/slack/format.js +58 -0
- package/dist/src/slack/format.js.map +1 -0
- package/dist/src/slack/forwarder.js +57 -0
- package/dist/src/slack/forwarder.js.map +1 -0
- package/dist/src/slack/register.js +13 -0
- package/dist/src/slack/register.js.map +1 -0
- package/dist/src/slack/send-handler.js +41 -0
- package/dist/src/slack/send-handler.js.map +1 -0
- package/dist/src/slack/send-only-client.js +46 -0
- package/dist/src/slack/send-only-client.js.map +1 -0
- package/dist/src/slack/slack-file.js +72 -0
- package/dist/src/slack/slack-file.js.map +1 -0
- package/dist/src/slack/socket.js +341 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/telegram/bot.js +117 -37
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/elicitation-buttons.js +7 -2
- package/dist/src/telegram/elicitation-buttons.js.map +1 -1
- package/dist/src/telegram/forwarder.js +15 -7
- package/dist/src/telegram/forwarder.js.map +1 -1
- package/dist/src/telegram/rich-message.js +209 -24
- package/dist/src/telegram/rich-message.js.map +1 -1
- package/dist/src/telegram/telegram-file.js +35 -4
- package/dist/src/telegram/telegram-file.js.map +1 -1
- package/dist/src/telegram/voice.js +4 -3
- package/dist/src/telegram/voice.js.map +1 -1
- package/package.json +5 -1
- package/public/assets/providers/slack.svg +1 -0
- package/public/dist/assets/ChannelsDiscord-D9FLnZso.js +1 -0
- package/public/dist/assets/ChannelsSlack-ybvupgdK.js +1 -0
- package/public/dist/assets/ChannelsTelegram-BOCOPrBc.js +1 -0
- package/public/dist/assets/HealthBadge-megTlF4n.js +1 -0
- package/public/dist/assets/TransportStatusChips-CjEySMis.js +1 -0
- package/public/dist/assets/app-BGIAy4he.js +55 -0
- package/public/dist/assets/{bgtask-badge-CdBqBy1L.js → bgtask-badge-DXca8E7c.js} +1 -1
- package/public/dist/assets/bgtask-badge-GRSiNILQ.js +1 -0
- package/public/dist/assets/{employees-C8qufeJl.js → employees-BL4qMxJj.js} +1 -1
- package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +1 -0
- package/public/dist/assets/{iframe-renderer-CZ67aeTO.js → iframe-renderer-DUI0LYBy.js} +2 -2
- package/public/dist/assets/{manager-CLXT60M5.js → manager-B-gpf-3j.js} +3 -3
- package/public/dist/assets/{memory-lWramLVw.js → memory-B_Y0L0KD.js} +1 -1
- package/public/dist/assets/memory-CX6YahTc.js +1 -0
- package/public/dist/assets/message-history-Det7u76S.js +1 -0
- package/public/dist/assets/{message-history-2NUN3UxM.js → message-history-Dl8dJzez.js} +1 -1
- package/public/dist/assets/{provider-icons-stsNmQKx.js → provider-icons-VDfahiUp.js} +3 -2
- package/public/dist/assets/{render-CsF-ojLe.js → render-BI1aqKoc.js} +1 -1
- package/public/dist/assets/{settings-BMLowRCh.js → settings-B0Sj4sUH.js} +1 -1
- package/public/dist/assets/settings-C7NutP6X.js +1 -0
- package/public/dist/assets/settings-core-CyX-px6_.js +100 -0
- package/public/dist/assets/settings-core-DVpFmrhY.js +1 -0
- package/public/dist/assets/{sidebar-gKjYLv49.js → sidebar-DOR9fTgN.js} +3 -3
- package/public/dist/assets/skills-BWhKreQ2.js +1 -0
- package/public/dist/assets/{skills-CKTbomdm.js → skills-wt0fkV9Y.js} +1 -1
- package/public/dist/assets/slash-commands-BZbqTg3v.js +20 -0
- package/public/dist/assets/slash-commands-Di4NDA4w.js +1 -0
- package/public/dist/assets/{trace-drawer-C3G4mmB1.js → trace-drawer-kMYzaVFW.js} +1 -1
- package/public/dist/assets/ui-BghexWrB.js +1 -0
- package/public/dist/assets/{ui-CVVC_flX.js → ui-D4D5HUfo.js} +1 -1
- package/public/dist/index.html +62 -1
- package/public/dist/manager/index.html +1 -1
- package/public/index.html +61 -0
- package/public/js/features/command-info.ts +1 -0
- package/public/js/features/help-content.ts +8 -0
- package/public/js/features/settings-channel.ts +5 -1
- package/public/js/features/settings-core.ts +2 -0
- package/public/js/features/settings-slack.ts +74 -0
- package/public/js/features/settings-types.ts +5 -1
- package/public/js/features/settings.ts +1 -0
- package/public/js/features/transport-status-row.ts +15 -3
- package/public/js/main.ts +19 -0
- package/public/js/provider-icons.ts +4 -1
- package/public/js/ws.ts +1 -1
- package/public/locales/en.json +21 -0
- package/public/locales/ja.json +21 -0
- package/public/locales/ko.json +21 -0
- package/public/locales/zh.json +21 -0
- package/public/manager/src/settings/SettingsShell.tsx +1 -0
- package/public/manager/src/settings/SettingsSidebar.tsx +1 -0
- package/public/manager/src/settings/pages/ChannelsSlack.tsx +323 -0
- package/public/manager/src/settings/pages/components/ActiveChannelToggle.tsx +2 -1
- package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +13 -4
- package/public/manager/src/settings/types.ts +1 -0
- package/scripts/check-redaction-sinks.mjs +276 -0
- package/scripts/pi-rpc-probe.mts +361 -0
- package/scripts/release-gates.mjs +83 -0
- package/scripts/release-preview.sh +8 -1
- package/scripts/release.sh +13 -2
- package/scripts/sync-electron-version.cjs +93 -0
- package/public/dist/assets/ChannelsDiscord-Bt1_rtNC.js +0 -1
- package/public/dist/assets/ChannelsTelegram-BLV0m1oq.js +0 -1
- package/public/dist/assets/TransportStatusChips-Bld0yXi7.js +0 -1
- package/public/dist/assets/app-iSJhsP20.js +0 -55
- package/public/dist/assets/bgtask-badge-CSkSsoB6.js +0 -1
- package/public/dist/assets/iframe-renderer-DXdo0YXN.js +0 -1
- package/public/dist/assets/memory-DeKUlD2M.js +0 -1
- package/public/dist/assets/message-history-CWTQ2woA.js +0 -1
- package/public/dist/assets/settings-CDjs7hTT.js +0 -1
- package/public/dist/assets/settings-core-Cs_oq-b3.js +0 -99
- package/public/dist/assets/settings-core-D-TOh1a6.js +0 -1
- package/public/dist/assets/skills-GuIfNASs.js +0 -1
- package/public/dist/assets/slash-commands-BtZa093n.js +0 -20
- package/public/dist/assets/slash-commands-DGJKfpMT.js +0 -1
- package/public/dist/assets/ui-DJg5Q_NJ.js +0 -1
- /package/public/dist/assets/{DashboardMeta-B7CVqXcU.js → DashboardMeta-DennaaA_.js} +0 -0
- /package/public/dist/assets/{Display-BF-rACK6.js → Display-hGuSn_W5.js} +0 -0
- /package/public/dist/assets/{Employees-D3hnciP0.js → Employees-B5YXk5ma.js} +0 -0
- /package/public/dist/assets/{Heartbeat-D4Pn5O3O.js → Heartbeat-vDGhPfct.js} +0 -0
- /package/public/dist/assets/{Mcp-SfytKWGX.js → Mcp-BHWnJ6SP.js} +0 -0
- /package/public/dist/assets/{Memory-DNxBOQ7N.js → Memory-B_UZ-iCA.js} +0 -0
- /package/public/dist/assets/{ModelProvider-CXwtlG7l.js → ModelProvider-CgUvs6pE.js} +0 -0
- /package/public/dist/assets/{Network-DISUhige.js → Network-AWpSkALD.js} +0 -0
- /package/public/dist/assets/{Profile-B8yBO3Ju.js → Profile-DnIE2ios.js} +0 -0
- /package/public/dist/assets/{Prompts-5lNQ5qOm.js → Prompts-B9aKLall.js} +0 -0
- /package/public/dist/assets/{SpeechKeys-BepaIHBa.js → SpeechKeys-K4Be_o7z.js} +0 -0
- /package/public/dist/assets/{TelegramHub--DNjRAwW.js → TelegramHub-CFwSkzHP.js} +0 -0
- /package/public/dist/assets/{TerminalPanel-DQovYfwz.js → TerminalPanel-CdlFZHAK.js} +0 -0
|
@@ -9,9 +9,10 @@ export type TransportStatus = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type ChannelHealth = {
|
|
12
|
-
activeInbound: 'telegram' | 'discord';
|
|
12
|
+
activeInbound: 'telegram' | 'discord' | 'slack';
|
|
13
13
|
telegram: TransportStatus;
|
|
14
14
|
discord: TransportStatus;
|
|
15
|
+
slack: TransportStatus;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
function isTransportStatus(value: unknown): value is TransportStatus {
|
|
@@ -27,12 +28,20 @@ export function parseChannelHealth(payload: unknown): ChannelHealth | null {
|
|
|
27
28
|
const channels = (payload as { channels?: unknown }).channels;
|
|
28
29
|
if (!channels || typeof channels !== 'object') return null;
|
|
29
30
|
const row = channels as Record<string, unknown>;
|
|
30
|
-
|
|
31
|
+
const active = row['activeInbound'];
|
|
32
|
+
if (active !== 'telegram' && active !== 'discord' && active !== 'slack') return null;
|
|
31
33
|
if (!isTransportStatus(row['telegram']) || !isTransportStatus(row['discord'])) return null;
|
|
34
|
+
// Slack is tolerated as absent: a newer Manager can be pointed at an older
|
|
35
|
+
// running cli-jaw, and rejecting the whole payload would hide Telegram and
|
|
36
|
+
// Discord health too.
|
|
37
|
+
const slack = isTransportStatus(row['slack'])
|
|
38
|
+
? row['slack']
|
|
39
|
+
: { configured: false, activeInbound: false, sendCapable: false, reason: 'unavailable' };
|
|
32
40
|
return {
|
|
33
|
-
activeInbound:
|
|
41
|
+
activeInbound: active,
|
|
34
42
|
telegram: row['telegram'],
|
|
35
43
|
discord: row['discord'],
|
|
44
|
+
slack,
|
|
36
45
|
};
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -46,7 +55,7 @@ export function transportChipLabels(status: TransportStatus): string[] {
|
|
|
46
55
|
|
|
47
56
|
type Props = {
|
|
48
57
|
client: SettingsClient;
|
|
49
|
-
channel: 'telegram' | 'discord';
|
|
58
|
+
channel: 'telegram' | 'discord' | 'slack';
|
|
50
59
|
};
|
|
51
60
|
|
|
52
61
|
export function TransportStatusChips({ client, channel }: Props) {
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fail when a channel sink reaches a user or a log without masking.
|
|
4
|
+
*
|
|
5
|
+
* Written because the audits kept finding the same class of miss: masking was
|
|
6
|
+
* added at call sites, a new call site appeared, and every green test suite
|
|
7
|
+
* still said the channels were covered. A grep proves nothing on its own, but
|
|
8
|
+
* it is a good alarm for "somebody added a reply that skips the masker", which
|
|
9
|
+
* is precisely what kept happening.
|
|
10
|
+
*
|
|
11
|
+
* The allowlist is the honest part. An exception has to name the line and the
|
|
12
|
+
* reason; an entry without one is a bug waiting to be found again.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Directories and single files whose outbound and logging surfaces must be masked.
|
|
22
|
+
*
|
|
23
|
+
* The channel folders are the obvious part. `src/messaging` is the shared layer
|
|
24
|
+
* every channel routes through, and `src/routes/messaging.ts` holds a Telegram
|
|
25
|
+
* `api.sendMessage` that lives outside the channel folders — the first version
|
|
26
|
+
* of this gate did not watch it, so a future edit there could have dropped the
|
|
27
|
+
* mask with no gate to notice.
|
|
28
|
+
*/
|
|
29
|
+
const SCANNED = [
|
|
30
|
+
'src/telegram',
|
|
31
|
+
'src/discord',
|
|
32
|
+
'src/slack',
|
|
33
|
+
'src/manager/telegram-hub',
|
|
34
|
+
'src/messaging',
|
|
35
|
+
'src/routes/messaging.ts',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Calls that put text in front of a user, or into a log file. */
|
|
39
|
+
const SINK_PATTERNS = [
|
|
40
|
+
{ name: 'chat reply', re: /\b(?:ctx\.reply|msg\.reply|channel\.send|editReply|answerCallbackQuery)\s*\(/ },
|
|
41
|
+
{ name: 'vendor send', re: /\bapi\.(?:sendMessage|editMessageText)\s*\(/ },
|
|
42
|
+
{ name: 'logger', re: /\b(?:log|appLog|console)\.(?:error|warn|info)\s*\(/ },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** A line counts as handled when it routes through one of these. */
|
|
46
|
+
const MASKERS = /\b(?:redactOutboundText|redactOutboundPayload|redactChannelSecrets|userErrorText|logErrorText)\s*\(/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lines that are safe without a masker, each with its reason.
|
|
50
|
+
*
|
|
51
|
+
* Matched on a distinctive substring rather than a line number, so ordinary
|
|
52
|
+
* edits above do not silently shift an exception onto a different line.
|
|
53
|
+
*/
|
|
54
|
+
const ALLOWLIST = [
|
|
55
|
+
// Fixed operator strings. The variable in them is a count, a limit or an
|
|
56
|
+
// id, none of which a credential travels in.
|
|
57
|
+
{ contains: 'Disabling Discord for this session', why: 'fixed string' },
|
|
58
|
+
{ contains: 'agent busy, queued', why: 'a pending count' },
|
|
59
|
+
{ contains: 'Max retries', why: 'a retry limit constant' },
|
|
60
|
+
{ contains: 'exceeds cap, giving up', why: 'elapsed milliseconds' },
|
|
61
|
+
{ contains: 'max reconnect attempts', why: 'an attempt limit constant' },
|
|
62
|
+
{ contains: 'the bot identity could not be read', why: 'fixed string' },
|
|
63
|
+
{ contains: 'channel not sendable', why: 'a channel id' },
|
|
64
|
+
{ contains: 'attempt ${attempt}/${MAX_RETRIES} failed', why: 'an attempt count and a vendor error code' },
|
|
65
|
+
// Vendor status text: a documented enum value such as invalid_auth or
|
|
66
|
+
// channel_not_found, not a message body.
|
|
67
|
+
{ contains: "auth.test failed:", why: 'a Slack error code' },
|
|
68
|
+
{ contains: 'apps.connections.open failed:', why: 'a Slack error code' },
|
|
69
|
+
{ contains: 'ack failed', why: 'a Slack error code' },
|
|
70
|
+
// Already masked by the channel-local helper that predates the shared one.
|
|
71
|
+
{ contains: 'redactSlackTokens(', why: 'masked by the Slack alias of the shared redactor' },
|
|
72
|
+
// Chunks and previews built from text the caller already masked.
|
|
73
|
+
{ contains: 'channel.send(chunk)', why: 'chunks come from chunkDiscordMessage, which masks' },
|
|
74
|
+
{ contains: '[discord:forward] → ${channelId}', why: 'preview is built from the masked text' },
|
|
75
|
+
{ contains: '[tg:forward] → chat ${chatId}', why: 'preview is built from the masked text' },
|
|
76
|
+
// Refusal notices whose reason is one of our own constants.
|
|
77
|
+
{ contains: '❌ ${result.reason}', why: 'reason is a fixed refusal string from the gateway' },
|
|
78
|
+
{ contains: 'msg.reply(warning)', why: 'warning is assembled from fixed attachment-failure strings' },
|
|
79
|
+
// Hub notices with no interpolation, and acks masked at their source.
|
|
80
|
+
{ contains: 'Topic not connected', why: 'fixed string' },
|
|
81
|
+
{ contains: 'Instance error', why: 'fixed string' },
|
|
82
|
+
{ contains: 'This topic is not connected', why: 'fixed string' },
|
|
83
|
+
{ contains: '이 토픽은 인스턴스에 연결되지', why: 'fixed string' },
|
|
84
|
+
{ contains: 'ctx.answerCallbackQuery(ack', why: 'ack is masked where it is built' },
|
|
85
|
+
{ contains: 'ctx.reply(syncText)', why: 'syncText is masked where it is built' },
|
|
86
|
+
{ contains: 'await ctx.reply(syncText)', why: 'syncText is masked where it is built' },
|
|
87
|
+
{ contains: 'Chat ID: <code>', why: 'a chat id the user just asked for' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Variables assigned from a masker earlier in the same file.
|
|
92
|
+
*
|
|
93
|
+
* `const out = redactOutboundText(...)` then `ctx.reply(out)` is correct, and
|
|
94
|
+
* flagging it teaches people to ignore the gate. This is a file-scoped
|
|
95
|
+
* approximation, not dataflow — it will miss a variable reassigned to
|
|
96
|
+
* something unsafe, which is a trade for a gate that stays believable.
|
|
97
|
+
*/
|
|
98
|
+
function maskedLocals(source) {
|
|
99
|
+
const names = new Set();
|
|
100
|
+
const re = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:redactOutboundText|redactOutboundPayload|redactChannelSecrets|userErrorText|logErrorText)\s*\(/g;
|
|
101
|
+
let m;
|
|
102
|
+
while ((m = re.exec(source)) !== null) names.add(m[1]);
|
|
103
|
+
return names;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listSources(dir) {
|
|
107
|
+
const abs = path.join(repoRoot, dir);
|
|
108
|
+
if (!fs.existsSync(abs)) return [];
|
|
109
|
+
if (fs.statSync(abs).isFile()) return [dir];
|
|
110
|
+
return fs.readdirSync(abs)
|
|
111
|
+
.filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))
|
|
112
|
+
.map((f) => path.join(dir, f));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether the line carries a value that could hold a credential.
|
|
117
|
+
*
|
|
118
|
+
* The first version flagged 62 lines, nearly all of them chat ids, counts and
|
|
119
|
+
* i18n lookups. A gate that cries wolf gets ignored, which is worse than not
|
|
120
|
+
* having one, so this asks the narrower question: does the line move text that
|
|
121
|
+
* came from an agent, a vendor, or a user?
|
|
122
|
+
*
|
|
123
|
+
* Those are the three sources a token has ever arrived from in this codebase.
|
|
124
|
+
* Structural values — ids, counts, filenames, translated strings — are not
|
|
125
|
+
* flagged, and a real leak through one of them would be a new class worth
|
|
126
|
+
* finding by other means.
|
|
127
|
+
*/
|
|
128
|
+
function carriesUntrustedText(line) {
|
|
129
|
+
return /\b(?:text|caption|content|message|body|result|output|reply|answer|prompt|preview|transcript|err|error|e)\b/.test(line)
|
|
130
|
+
// A translated string is built from a catalogue, not from input.
|
|
131
|
+
&& !/\bt\(['"]/.test(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function findUnmaskedSinks() {
|
|
135
|
+
const findings = [];
|
|
136
|
+
for (const dir of SCANNED) {
|
|
137
|
+
for (const rel of listSources(dir)) {
|
|
138
|
+
const source = fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
|
139
|
+
const masked = maskedLocals(source);
|
|
140
|
+
const lines = source.split('\n');
|
|
141
|
+
lines.forEach((line, i) => {
|
|
142
|
+
const trimmed = line.trim();
|
|
143
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) return;
|
|
144
|
+
const sink = SINK_PATTERNS.find((p) => p.re.test(line));
|
|
145
|
+
if (!sink) return;
|
|
146
|
+
if (MASKERS.test(line)) return;
|
|
147
|
+
if ([...masked].some((name) => new RegExp(`\\b${name}\\b`).test(line))) return;
|
|
148
|
+
if (!carriesUntrustedText(line)) return;
|
|
149
|
+
if (ALLOWLIST.some((a) => line.includes(a.contains))) return;
|
|
150
|
+
findings.push({ file: rel, line: i + 1, kind: sink.name, text: trimmed.slice(0, 100) });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Functions every outbound text of a channel must pass through, and which
|
|
159
|
+
* therefore have to keep calling a masker.
|
|
160
|
+
*
|
|
161
|
+
* Most call sites are allowlisted on the grounds that "the chunker masks", and
|
|
162
|
+
* for a while nothing checked that the chunker still did. Deleting the
|
|
163
|
+
* `redactOutboundText(...)` inside `chunkDiscordMessage` unmasked every Discord
|
|
164
|
+
* send in the codebase and the gate stayed green, because each individual call
|
|
165
|
+
* site still looked exactly as approved.
|
|
166
|
+
*
|
|
167
|
+
* These are the funnels. If one stops masking, the allowlist entries that lean
|
|
168
|
+
* on it become false, so the funnel is checked directly.
|
|
169
|
+
*/
|
|
170
|
+
const REQUIRED_MASKING_FUNNELS = [
|
|
171
|
+
{ file: 'src/discord/forwarder.ts', fn: 'chunkDiscordMessage', why: 'every outbound Discord text is chunked here' },
|
|
172
|
+
{ file: 'src/slack/format.ts', fn: 'chunkSlackMessage', why: 'every outbound Slack text is chunked here' },
|
|
173
|
+
{ file: 'src/telegram/rich-message.ts', fn: 'sendTelegramMarkdown', why: 'the single entry point for Telegram rich sends' },
|
|
174
|
+
// `sendTelegramFile` masks two independent things: the caption on the way
|
|
175
|
+
// out, and the vendor error on the way back. Checking only that the body
|
|
176
|
+
// mentions SOME masker lets the caption mask be deleted while the error
|
|
177
|
+
// mask keeps the gate green, so this one names the expression.
|
|
178
|
+
{
|
|
179
|
+
file: 'src/telegram/telegram-file.ts',
|
|
180
|
+
fn: 'sendTelegramFile',
|
|
181
|
+
expect: /caption\s*=\s*opts\?\.caption\s*===\s*undefined\s*\?\s*undefined\s*:\s*redactOutboundText\(/,
|
|
182
|
+
why: 'a caption reaches the room like a message body',
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Body of a top-level function declaration, by brace balance.
|
|
188
|
+
*
|
|
189
|
+
* Finding the body is the fiddly part: the first `{` after the name belongs to
|
|
190
|
+
* an inline parameter type (`opts?: { caption?: string }`), and the second to a
|
|
191
|
+
* return type (`Promise<{ ok: boolean }>`). So the parameter list is skipped by
|
|
192
|
+
* paren depth, and the return annotation by angle depth, leaving the real body
|
|
193
|
+
* brace. A parser would be more correct and would also make this gate something
|
|
194
|
+
* people stop reading.
|
|
195
|
+
*/
|
|
196
|
+
function functionBody(source, fn) {
|
|
197
|
+
const start = source.search(new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${fn}\\s*[(<]`));
|
|
198
|
+
if (start === -1) return null;
|
|
199
|
+
// Step 1: walk the parameter list to its closing paren.
|
|
200
|
+
const paren = source.indexOf('(', start);
|
|
201
|
+
if (paren === -1) return null;
|
|
202
|
+
let parenDepth = 0;
|
|
203
|
+
let afterParams = -1;
|
|
204
|
+
for (let i = paren; i < source.length; i += 1) {
|
|
205
|
+
if (source[i] === '(') parenDepth += 1;
|
|
206
|
+
else if (source[i] === ')') {
|
|
207
|
+
parenDepth -= 1;
|
|
208
|
+
if (parenDepth === 0) { afterParams = i + 1; break; }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (afterParams === -1) return null;
|
|
212
|
+
// Step 2: the body brace is the first `{` outside any return-type generic.
|
|
213
|
+
let angle = 0;
|
|
214
|
+
let open = -1;
|
|
215
|
+
for (let i = afterParams; i < source.length; i += 1) {
|
|
216
|
+
const c = source[i];
|
|
217
|
+
if (c === '<') angle += 1;
|
|
218
|
+
else if (c === '>') angle = Math.max(0, angle - 1);
|
|
219
|
+
else if (c === '{' && angle === 0) { open = i; break; }
|
|
220
|
+
}
|
|
221
|
+
if (open === -1) return null;
|
|
222
|
+
let depth = 0;
|
|
223
|
+
for (let i = open; i < source.length; i += 1) {
|
|
224
|
+
if (source[i] === '{') depth += 1;
|
|
225
|
+
else if (source[i] === '}') {
|
|
226
|
+
depth -= 1;
|
|
227
|
+
if (depth === 0) return source.slice(open, i + 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function findUnmaskedFunnels() {
|
|
234
|
+
const findings = [];
|
|
235
|
+
for (const funnel of REQUIRED_MASKING_FUNNELS) {
|
|
236
|
+
const abs = path.join(repoRoot, funnel.file);
|
|
237
|
+
if (!fs.existsSync(abs)) {
|
|
238
|
+
findings.push({ ...funnel, detail: 'file is gone — the funnel moved and this gate was not updated' });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const body = functionBody(fs.readFileSync(abs, 'utf8'), funnel.fn);
|
|
242
|
+
if (body === null) {
|
|
243
|
+
findings.push({ ...funnel, detail: 'function not found — it was renamed or removed' });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const required = funnel.expect ?? MASKERS;
|
|
247
|
+
if (!required.test(body)) {
|
|
248
|
+
findings.push({
|
|
249
|
+
...funnel,
|
|
250
|
+
detail: funnel.expect ? 'the specific masking expression is gone' : 'body no longer calls a masker',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return findings;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
258
|
+
const findings = findUnmaskedSinks();
|
|
259
|
+
const funnels = findUnmaskedFunnels();
|
|
260
|
+
if (findings.length === 0 && funnels.length === 0) {
|
|
261
|
+
console.log('✅ redaction sinks — every channel reply, send and logger routes through a masker');
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
if (findings.length > 0) {
|
|
265
|
+
console.error(`❌ redaction sinks — ${findings.length} unmasked:`);
|
|
266
|
+
for (const f of findings) console.error(` ${f.file}:${f.line} (${f.kind}) ${f.text}`);
|
|
267
|
+
console.error('\nEither route it through a masker, or add an allowlist entry WITH a reason.');
|
|
268
|
+
}
|
|
269
|
+
if (funnels.length > 0) {
|
|
270
|
+
console.error(`❌ masking funnels — ${funnels.length} broken:`);
|
|
271
|
+
for (const f of funnels) console.error(` ${f.file} ${f.fn}(): ${f.detail}\n ${f.why}`);
|
|
272
|
+
console.error('\nAllowlist entries below assume these funnels mask. Restore the mask, or');
|
|
273
|
+
console.error('move the funnel and update REQUIRED_MASKING_FUNNELS to match.');
|
|
274
|
+
}
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|