@0xmaxma/claude-gateway 1.3.24 → 1.3.31
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 +172 -50
- package/config.template.json +6 -2
- package/dist/agent/incident-store.d.ts +89 -0
- package/dist/agent/incident-store.d.ts.map +1 -0
- package/dist/agent/incident-store.js +299 -0
- package/dist/agent/incident-store.js.map +1 -0
- package/dist/agent/incident.d.ts +156 -0
- package/dist/agent/incident.d.ts.map +1 -0
- package/dist/agent/incident.js +177 -0
- package/dist/agent/incident.js.map +1 -0
- package/dist/agent/recovery-executor.d.ts +117 -0
- package/dist/agent/recovery-executor.d.ts.map +1 -0
- package/dist/agent/recovery-executor.js +168 -0
- package/dist/agent/recovery-executor.js.map +1 -0
- package/dist/agent/recovery-policy.d.ts +97 -0
- package/dist/agent/recovery-policy.d.ts.map +1 -0
- package/dist/agent/recovery-policy.js +164 -0
- package/dist/agent/recovery-policy.js.map +1 -0
- package/dist/agent/runner.d.ts +44 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +272 -2
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/safe-mode.d.ts +61 -0
- package/dist/agent/safe-mode.d.ts.map +1 -0
- package/dist/agent/safe-mode.js +102 -0
- package/dist/agent/safe-mode.js.map +1 -0
- package/dist/agent/triage.d.ts +94 -0
- package/dist/agent/triage.d.ts.map +1 -0
- package/dist/agent/triage.js +209 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/agent/turn-trace.d.ts +120 -0
- package/dist/agent/turn-trace.d.ts.map +1 -0
- package/dist/agent/turn-trace.js +122 -0
- package/dist/agent/turn-trace.js.map +1 -0
- package/dist/api/gateway-router.d.ts +21 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +58 -17
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-pending-senders.d.ts +7 -2
- package/dist/api/line-pending-senders.d.ts.map +1 -1
- package/dist/api/line-pending-senders.js +7 -2
- package/dist/api/line-pending-senders.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +563 -172
- package/dist/api/router.js.map +1 -1
- package/dist/api/wizard-state.d.ts +1 -4
- package/dist/api/wizard-state.d.ts.map +1 -1
- package/dist/api/wizard-state.js.map +1 -1
- package/dist/config/migrator.d.ts +4 -0
- package/dist/config/migrator.d.ts.map +1 -1
- package/dist/config/migrator.js +60 -3
- package/dist/config/migrator.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +24 -6
- package/dist/discord/receiver.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +18 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +61 -1
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +59 -0
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/control-channel.d.ts +74 -0
- package/dist/shell/control-channel.d.ts.map +1 -0
- package/dist/shell/control-channel.js +114 -0
- package/dist/shell/control-channel.js.map +1 -0
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +18 -6
- package/dist/telegram/receiver.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +103 -2
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/access.ts +136 -20
- package/mcp/tools/discord/client.ts +3 -1
- package/mcp/tools/discord/module.ts +75 -4
- package/mcp/tools/discord/skills/access/SKILL.md +66 -8
- package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
- package/mcp/tools/discord/types.ts +21 -2
- package/mcp/tools/skills/handlers.ts +4 -2
- package/mcp/tools/telegram/dedup.ts +4 -1
- package/mcp/tools/telegram/module.ts +5 -14
- package/mcp/tools/telegram/pure.ts +169 -31
- package/mcp/tools/telegram/receiver-server.ts +271 -78
- package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
- package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
- package/mcp/tools/telegram/typing.ts +124 -0
- package/package.json +3 -1
|
@@ -9,17 +9,31 @@ export type PendingEntry = {
|
|
|
9
9
|
createdAt: number
|
|
10
10
|
expiresAt: number
|
|
11
11
|
replies: number
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
requireMention: boolean
|
|
16
|
-
allowFrom: string[]
|
|
12
|
+
// Absent ⇒ 'dm'. A 'group' entry is a group knock: chatId holds the group id
|
|
13
|
+
// and approval pushes that id into groupAllowlist (mirrors LINE).
|
|
14
|
+
kind?: 'dm' | 'group'
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export type Access = {
|
|
20
|
-
dmPolicy: '
|
|
18
|
+
dmPolicy: 'open' | 'allowlist' | 'disabled'
|
|
19
|
+
// Orthogonal to dmPolicy (mirrors LINE): when the base policy is 'allowlist',
|
|
20
|
+
// `pairing: true` means an unknown sender gets a one-time code and lands in
|
|
21
|
+
// pending for the admin to approve; `pairing: false` means they're silently
|
|
22
|
+
// dropped (pure allowlist). Ignored when dmPolicy is 'open'/'disabled'.
|
|
23
|
+
pairing: boolean
|
|
21
24
|
allowFrom: string[]
|
|
22
|
-
|
|
25
|
+
// Group access tier (mirrors LINE): base policy for groups, the allowlisted
|
|
26
|
+
// group ids, and a single requireMention gate. `pairing` (above) governs
|
|
27
|
+
// group code-minting too, exactly like DMs.
|
|
28
|
+
groupPolicy: 'open' | 'allowlist' | 'disabled'
|
|
29
|
+
groupAllowlist: string[]
|
|
30
|
+
requireMention: boolean
|
|
31
|
+
// Migration-only artifact: a pre-split file's per-group `allowFrom` (the old
|
|
32
|
+
// schema could restrict a group to specific senders). The new model has no
|
|
33
|
+
// per-user group tier, so this is preserved but never written by any current
|
|
34
|
+
// API/CLI — enforced in gateLogic() as an extra filter so migrating doesn't
|
|
35
|
+
// silently widen a previously-restricted group to every member.
|
|
36
|
+
legacyGroupAllowFrom?: Record<string, string[]>
|
|
23
37
|
pending: Record<string, PendingEntry>
|
|
24
38
|
mentionPatterns?: string[]
|
|
25
39
|
ackReaction?: string
|
|
@@ -28,15 +42,101 @@ export type Access = {
|
|
|
28
42
|
chunkMode?: 'length' | 'newline'
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
// Default for a brand-new agent (no access.json yet): closed base + pairing on,
|
|
46
|
+
// so the owner can DM the bot and capture their own id via a code. This matches
|
|
47
|
+
// the pre-split behavior where a missing file defaulted to dmPolicy:'pairing'.
|
|
31
48
|
export function defaultAccess(): Access {
|
|
32
49
|
return {
|
|
33
|
-
dmPolicy: '
|
|
50
|
+
dmPolicy: 'allowlist',
|
|
51
|
+
pairing: true,
|
|
34
52
|
allowFrom: [],
|
|
35
|
-
|
|
53
|
+
groupPolicy: 'allowlist',
|
|
54
|
+
groupAllowlist: [],
|
|
55
|
+
requireMention: true,
|
|
36
56
|
pending: {},
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a parsed access.json into the current shape, migrating the legacy
|
|
62
|
+
* 4-value dmPolicy (which folded pairing in) to the split model.
|
|
63
|
+
*
|
|
64
|
+
* SECURITY: a legacy `allowlist` file was deliberately locked down — it must
|
|
65
|
+
* migrate to `pairing:false`, NOT true, or it would start minting codes for
|
|
66
|
+
* strangers. `pairing ?? true` is only correct for the brand-new/ENOENT path
|
|
67
|
+
* (see defaultAccess). Here an absent `pairing` on an existing file means a
|
|
68
|
+
* pre-split file → pairing off.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Extract per-group sender restrictions from a legacy `groups` map. Only a
|
|
72
|
+
* non-empty `allowFrom` counts as a restriction — an empty array meant
|
|
73
|
+
* "unrestricted" under the old schema, same as it does now.
|
|
74
|
+
*/
|
|
75
|
+
export function deriveLegacyGroupAllowFrom(
|
|
76
|
+
groups?: Record<string, { requireMention?: boolean; allowFrom?: string[] }>,
|
|
77
|
+
): Record<string, string[]> | undefined {
|
|
78
|
+
if (!groups) return undefined
|
|
79
|
+
const out: Record<string, string[]> = {}
|
|
80
|
+
for (const [groupId, g] of Object.entries(groups)) {
|
|
81
|
+
if (g.allowFrom && g.allowFrom.length > 0) out[groupId] = [...g.allowFrom]
|
|
82
|
+
}
|
|
83
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function migrateAccess(parsed: {
|
|
87
|
+
dmPolicy?: string
|
|
88
|
+
pairing?: boolean
|
|
89
|
+
allowFrom?: string[]
|
|
90
|
+
groups?: Record<string, { requireMention?: boolean; allowFrom?: string[] }>
|
|
91
|
+
groupPolicy?: string
|
|
92
|
+
groupAllowlist?: string[]
|
|
93
|
+
requireMention?: boolean
|
|
94
|
+
legacyGroupAllowFrom?: Record<string, string[]>
|
|
95
|
+
pending?: Record<string, PendingEntry>
|
|
96
|
+
mentionPatterns?: string[]
|
|
97
|
+
ackReaction?: string
|
|
98
|
+
replyToMode?: 'off' | 'first' | 'all'
|
|
99
|
+
textChunkLimit?: number
|
|
100
|
+
chunkMode?: 'length' | 'newline'
|
|
101
|
+
}): Access {
|
|
102
|
+
const legacy = parsed.dmPolicy
|
|
103
|
+
let dmPolicy: Access['dmPolicy']
|
|
104
|
+
let pairing: boolean
|
|
105
|
+
if (legacy === 'pairing') {
|
|
106
|
+
dmPolicy = 'allowlist'
|
|
107
|
+
pairing = true
|
|
108
|
+
} else {
|
|
109
|
+
dmPolicy = (legacy as Access['dmPolicy']) ?? 'allowlist'
|
|
110
|
+
pairing = parsed.pairing ?? false
|
|
111
|
+
}
|
|
112
|
+
// Group tier: flatten legacy per-group `groups` map to a flat allowlist
|
|
113
|
+
// (mirrors LINE). Legacy groups were closed-by-default → migrate to
|
|
114
|
+
// 'allowlist' + requireMention:true, behavior-preserving. A group's
|
|
115
|
+
// per-user `allowFrom` override has no equivalent in the new flat model,
|
|
116
|
+
// but it's still a real restriction — preserve it in legacyGroupAllowFrom
|
|
117
|
+
// and enforce it in gateLogic() rather than silently dropping it (that
|
|
118
|
+
// would widen a restricted group to every member).
|
|
119
|
+
const groupAllowlist = parsed.groupAllowlist ?? Object.keys(parsed.groups ?? {})
|
|
120
|
+
const groupPolicy = (parsed.groupPolicy as Access['groupPolicy']) ?? 'allowlist'
|
|
121
|
+
const requireMention = parsed.requireMention ?? true
|
|
122
|
+
const legacyGroupAllowFrom = parsed.legacyGroupAllowFrom ?? deriveLegacyGroupAllowFrom(parsed.groups)
|
|
123
|
+
return {
|
|
124
|
+
dmPolicy,
|
|
125
|
+
pairing,
|
|
126
|
+
allowFrom: parsed.allowFrom ?? [],
|
|
127
|
+
groupPolicy,
|
|
128
|
+
groupAllowlist,
|
|
129
|
+
requireMention,
|
|
130
|
+
legacyGroupAllowFrom,
|
|
131
|
+
pending: parsed.pending ?? {},
|
|
132
|
+
mentionPatterns: parsed.mentionPatterns,
|
|
133
|
+
ackReaction: parsed.ackReaction,
|
|
134
|
+
replyToMode: parsed.replyToMode,
|
|
135
|
+
textChunkLimit: parsed.textChunkLimit,
|
|
136
|
+
chunkMode: parsed.chunkMode,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
export const MAX_CHUNK_LIMIT = 4096
|
|
41
141
|
|
|
42
142
|
export function pruneExpired(a: Access, now?: number): boolean {
|
|
@@ -81,18 +181,8 @@ import { join } from 'path'
|
|
|
81
181
|
export function readAccessFile(accessFile: string): Access {
|
|
82
182
|
try {
|
|
83
183
|
const raw = readFileSync(accessFile, 'utf8')
|
|
84
|
-
const parsed = JSON.parse(raw) as Partial<Access>
|
|
85
|
-
return
|
|
86
|
-
dmPolicy: parsed.dmPolicy ?? 'pairing',
|
|
87
|
-
allowFrom: parsed.allowFrom ?? [],
|
|
88
|
-
groups: parsed.groups ?? {},
|
|
89
|
-
pending: parsed.pending ?? {},
|
|
90
|
-
mentionPatterns: parsed.mentionPatterns,
|
|
91
|
-
ackReaction: parsed.ackReaction,
|
|
92
|
-
replyToMode: parsed.replyToMode,
|
|
93
|
-
textChunkLimit: parsed.textChunkLimit,
|
|
94
|
-
chunkMode: parsed.chunkMode,
|
|
95
|
-
}
|
|
184
|
+
const parsed = JSON.parse(raw) as Partial<Access> & { dmPolicy?: string }
|
|
185
|
+
return migrateAccess(parsed)
|
|
96
186
|
} catch (err) {
|
|
97
187
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return defaultAccess()
|
|
98
188
|
try {
|
|
@@ -124,7 +214,7 @@ export type GateInput = {
|
|
|
124
214
|
export type GateResult =
|
|
125
215
|
| { action: 'deliver'; access: Access }
|
|
126
216
|
| { action: 'drop' }
|
|
127
|
-
| { action: 'pair'; code: string; isResend: boolean }
|
|
217
|
+
| { action: 'pair'; code: string; isResend: boolean; isGroup?: boolean }
|
|
128
218
|
|
|
129
219
|
/**
|
|
130
220
|
* Pure gate logic (for testing without Grammy Context).
|
|
@@ -149,19 +239,29 @@ export function gateLogic(
|
|
|
149
239
|
const chatType = input.chatType
|
|
150
240
|
|
|
151
241
|
if (chatType === 'private') {
|
|
242
|
+
if (access.dmPolicy === 'open') {
|
|
243
|
+
if (!access.allowFrom.includes(senderId)) {
|
|
244
|
+
access.allowFrom.push(senderId)
|
|
245
|
+
saveAccessFn(access)
|
|
246
|
+
}
|
|
247
|
+
return { action: 'deliver', access }
|
|
248
|
+
}
|
|
152
249
|
if (access.allowFrom.includes(senderId)) return { action: 'deliver', access }
|
|
153
|
-
|
|
250
|
+
// Base policy is 'allowlist' ('disabled' already dropped above). Pairing is
|
|
251
|
+
// the orthogonal toggle: off ⇒ pure allowlist (drop strangers, no code).
|
|
252
|
+
if (!access.pairing) return { action: 'drop' }
|
|
154
253
|
|
|
155
254
|
// pairing mode
|
|
156
255
|
for (const [code, p] of Object.entries(access.pending)) {
|
|
157
|
-
if (p.senderId === senderId) {
|
|
256
|
+
if ((p.kind ?? 'dm') === 'dm' && p.senderId === senderId) {
|
|
158
257
|
if ((p.replies ?? 1) >= 2) return { action: 'drop' }
|
|
159
258
|
p.replies = (p.replies ?? 1) + 1
|
|
160
259
|
saveAccessFn(access)
|
|
161
260
|
return { action: 'pair', code, isResend: true }
|
|
162
261
|
}
|
|
163
262
|
}
|
|
164
|
-
|
|
263
|
+
// Cap pending per-kind so group knocks and DM knocks don't starve each other.
|
|
264
|
+
if (countPending(access, 'dm') >= 5) return { action: 'drop' }
|
|
165
265
|
|
|
166
266
|
const code = generateCode()
|
|
167
267
|
const ts = now ?? Date.now()
|
|
@@ -171,6 +271,7 @@ export function gateLogic(
|
|
|
171
271
|
createdAt: ts,
|
|
172
272
|
expiresAt: ts + 60 * 60 * 1000,
|
|
173
273
|
replies: 1,
|
|
274
|
+
kind: 'dm',
|
|
174
275
|
}
|
|
175
276
|
saveAccessFn(access)
|
|
176
277
|
return { action: 'pair', code, isResend: false }
|
|
@@ -178,14 +279,42 @@ export function gateLogic(
|
|
|
178
279
|
|
|
179
280
|
if (chatType === 'group' || chatType === 'supergroup') {
|
|
180
281
|
const groupId = input.chatId ?? ''
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
282
|
+
if (access.groupPolicy === 'disabled') return { action: 'drop' }
|
|
283
|
+
|
|
284
|
+
if (access.groupPolicy === 'allowlist' && !access.groupAllowlist.includes(groupId)) {
|
|
285
|
+
// Unknown group. Pairing off ⇒ silent drop (pure allowlist). On ⇒ mint a
|
|
286
|
+
// code keyed on the group id; a member relays it to the admin (mirrors LINE).
|
|
287
|
+
if (!access.pairing) return { action: 'drop' }
|
|
288
|
+
for (const [code, p] of Object.entries(access.pending)) {
|
|
289
|
+
if (p.kind === 'group' && p.chatId === groupId) {
|
|
290
|
+
if ((p.replies ?? 1) >= 2) return { action: 'drop' }
|
|
291
|
+
p.replies = (p.replies ?? 1) + 1
|
|
292
|
+
saveAccessFn(access)
|
|
293
|
+
return { action: 'pair', code, isResend: true, isGroup: true }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (countPending(access, 'group') >= 5) return { action: 'drop' }
|
|
297
|
+
const code = generateCode()
|
|
298
|
+
const ts = now ?? Date.now()
|
|
299
|
+
access.pending[code] = {
|
|
300
|
+
senderId,
|
|
301
|
+
chatId: groupId,
|
|
302
|
+
createdAt: ts,
|
|
303
|
+
expiresAt: ts + 60 * 60 * 1000,
|
|
304
|
+
replies: 1,
|
|
305
|
+
kind: 'group',
|
|
306
|
+
}
|
|
307
|
+
saveAccessFn(access)
|
|
308
|
+
return { action: 'pair', code, isResend: false, isGroup: true }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Allowlisted (or open policy) → enforce any legacy per-sender restriction
|
|
312
|
+
// that survived migration, then the single mention gate.
|
|
313
|
+
const legacyAllowed = access.legacyGroupAllowFrom?.[groupId]
|
|
314
|
+
if (legacyAllowed && legacyAllowed.length > 0 && !legacyAllowed.includes(senderId)) {
|
|
186
315
|
return { action: 'drop' }
|
|
187
316
|
}
|
|
188
|
-
if (requireMention && !isMentionedPure(input, access.mentionPatterns)) {
|
|
317
|
+
if (access.requireMention !== false && !isMentionedPure(input, access.mentionPatterns)) {
|
|
189
318
|
return { action: 'drop' }
|
|
190
319
|
}
|
|
191
320
|
return { action: 'deliver', access }
|
|
@@ -194,6 +323,15 @@ export function gateLogic(
|
|
|
194
323
|
return { action: 'drop' }
|
|
195
324
|
}
|
|
196
325
|
|
|
326
|
+
/** Count pending entries of a given kind (absent kind ⇒ 'dm'). */
|
|
327
|
+
function countPending(access: Access, kind: 'dm' | 'group'): number {
|
|
328
|
+
let n = 0
|
|
329
|
+
for (const p of Object.values(access.pending)) {
|
|
330
|
+
if ((p.kind ?? 'dm') === kind) n++
|
|
331
|
+
}
|
|
332
|
+
return n
|
|
333
|
+
}
|
|
334
|
+
|
|
197
335
|
export { hasMarkdown, toTelegramHtml } from './lib/markdown'
|
|
198
336
|
|
|
199
337
|
export function isMentionedPure(input: GateInput, extraPatterns?: string[]): boolean {
|